package node_test

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

	"google.golang.org/protobuf/proto"

	nodepkg "iop/apps/node/internal/node"
	"iop/apps/node/internal/workspace"
	iop "iop/proto/gen/iop"
)

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

func TestNodeWorkspaceCommandHelperProcess(t *testing.T) {
	mode := os.Getenv("IOP_NODE_WORKSPACE_HELPER")
	if mode == "" {
		return
	}
	switch mode {
	case "success":
		_, _ = fmt.Fprint(os.Stdout, "node-command-stdout")
		_, _ = fmt.Fprint(os.Stderr, "node-command-stderr")
	case "block":
		if err := os.WriteFile(os.Getenv("IOP_NODE_START_FILE"), []byte("started"), 0o600); err != nil {
			os.Exit(21)
		}
		for {
			time.Sleep(time.Hour)
		}
	default:
		os.Exit(22)
	}
	os.Exit(0)
}

func workspaceRuntimeForNode(t *testing.T) (*workspace.Runtime, string) {
	t.Helper()
	root := t.TempDir()
	runtime, err := workspace.NewRuntime([]*iop.WorkspaceConfig{{
		Ref: "workspace-1", Platform: "darwin", Root: root,
		Operations:   []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE},
		MaxReadBytes: 64, MaxWriteBytes: 64, MaxOutputBytes: 64,
	}}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = runtime.Close() })
	return runtime, root
}

func workspaceOpenForNode(requestID string) *iop.WorkspaceOpenRequest {
	return &iop.WorkspaceOpenRequest{
		RequestId: requestID, WorkspaceRef: "workspace-1",
		Operations: []iop.WorkspaceOperation{
			iop.WorkspaceOperation_WORKSPACE_OPERATION_READ,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
			iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE,
		},
		MaxReadBytes: 64, MaxWriteBytes: 64, MaxOutputBytes: 64,
	}
}

func TestNodeWorkspaceOpenAndFileMapping(t *testing.T) {
	n, _ := makeNode(t, nil)
	runtime, root := workspaceRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	if err := os.WriteFile(filepath.Join(root, "input.txt"), []byte("ok"), 0600); err != nil {
		t.Fatal(err)
	}
	opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-1"))
	if err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || opened.GetRequestId() != "request-1" {
		t.Fatalf("open=%+v err=%v", opened, err)
	}
	read, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{RequestId: "request-1", StageId: "work", ToolCallId: "read-1", Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, Input: &iop.WorkspaceToolRequest_RelativePath{RelativePath: "input.txt"}})
	if err != nil || read.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(read.GetContent()) != "ok" {
		t.Fatalf("read=%+v err=%v", read, err)
	}
	write, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
		RequestId: "request-1", StageId: "work", ToolCallId: "write-1",
		Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
		Input:     &iop.WorkspaceToolRequest_Write{Write: &iop.WorkspaceWriteInput{RelativePath: "output.txt", Content: []byte("written")}},
	})
	if err != nil || write.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write=%+v err=%v", write, err)
	}
	written, readErr := os.ReadFile(filepath.Join(root, "output.txt"))
	if readErr != nil || string(written) != "written" {
		t.Fatalf("written=%q err=%v", written, readErr)
	}
	oversized, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
		RequestId: "request-1", StageId: "work", ToolCallId: "write-oversized",
		Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
		Input:     &iop.WorkspaceToolRequest_Write{Write: &iop.WorkspaceWriteInput{RelativePath: "oversized.txt", Content: []byte(strings.Repeat("x", 65))}},
	})
	if err != nil || oversized.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("oversized=%+v err=%v", oversized, err)
	}
	for name, request := range map[string]func() *iop.WorkspaceToolRequest{
		"legacy": func() *iop.WorkspaceToolRequest {
			return &iop.WorkspaceToolRequest{Input: &iop.WorkspaceToolRequest_WriteContent{WriteContent: []byte("legacy")}}
		},
		"path-only": func() *iop.WorkspaceToolRequest {
			return &iop.WorkspaceToolRequest{Input: &iop.WorkspaceToolRequest_RelativePath{RelativePath: "legacy.txt"}}
		},
		"nil-structured": func() *iop.WorkspaceToolRequest {
			return &iop.WorkspaceToolRequest{Input: &iop.WorkspaceToolRequest_Write{}}
		},
	} {
		t.Run(name, func(t *testing.T) {
			toolRequest := request()
			toolRequest.RequestId, toolRequest.StageId, toolRequest.ToolCallId = "request-1", "work", "bad-write"
			toolRequest.Operation = iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE
			response, err := n.OnWorkspaceTool(context.Background(), nil, toolRequest)
			if err != nil || response.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
				t.Fatalf("response=%+v err=%v", response, err)
			}
		})
	}
	command, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{RequestId: "request-1", StageId: "work", ToolCallId: "command-1", Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND})
	if err != nil || command.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || command.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("command=%+v err=%v", command, err)
	}
}

func TestNodeWorkspaceArtifactMapping(t *testing.T) {
	n, _ := makeNode(t, nil)
	runtime, root := workspaceRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	if opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-artifact")); err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("open = %+v, %v", opened, err)
	}

	readPlan := &iop.WorkspaceArtifactRequest{
		RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN,
		Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
	}
	missing, err := n.OnWorkspaceArtifact(context.Background(), nil, readPlan)
	if err != nil || missing.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || missing.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND || missing.GetContent() != nil {
		t.Fatalf("missing plan = %+v, %v", missing, err)
	}
	writePlan := &iop.WorkspaceArtifactRequest{
		RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN,
		Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE, Content: []byte("bounded plan"),
	}
	written, err := n.OnWorkspaceArtifact(context.Background(), nil, writePlan)
	if err != nil || written.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || len(written.GetContent()) != 0 {
		t.Fatalf("write plan = %+v, %v", written, err)
	}
	read, err := n.OnWorkspaceArtifact(context.Background(), nil, readPlan)
	if err != nil || read.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(read.GetContent()) != "bounded plan" {
		t.Fatalf("read plan = %+v, %v", read, err)
	}
	if data, err := os.ReadFile(filepath.Join(root, ".iop", "job", "request-artifact", "plan.md")); err != nil || string(data) != "bounded plan" {
		t.Fatalf("mapped plan = %q, %v", data, err)
	}

	for name, request := range map[string]*iop.WorkspaceArtifactRequest{
		"unknown-kind": {
			RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_UNSPECIFIED,
			Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
		},
		"unknown-operation": {
			RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW,
			Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_UNSPECIFIED,
		},
		"read-content": {
			RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW,
			Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ, Content: []byte("must not be accepted"),
		},
		"oversized-write": {
			RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW,
			Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE, Content: []byte(strings.Repeat("x", 1<<20+1)),
		},
	} {
		t.Run(name, func(t *testing.T) {
			response, callErr := n.OnWorkspaceArtifact(context.Background(), nil, request)
			if callErr != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || response.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST || len(response.GetContent()) != 0 {
				t.Fatalf("response = %+v, %v", response, callErr)
			}
		})
	}
	if _, err := os.Stat(filepath.Join(root, ".iop", "job", "request-artifact", "review.md")); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("malformed artifact request created review.md: %v", err)
	}
}

func TestNodeWorkspaceArtifactStableFailures(t *testing.T) {
	n, _ := makeNode(t, nil)
	request := &iop.WorkspaceArtifactRequest{
		RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN,
		Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
	}
	missingRuntime, err := n.OnWorkspaceArtifact(context.Background(), nil, request)
	if err != nil || missingRuntime.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED || missingRuntime.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
		t.Fatalf("missing runtime = %+v, %v", missingRuntime, err)
	}
	invalid, err := n.OnWorkspaceArtifact(context.Background(), nil, nil)
	if err != nil || invalid.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("nil request = %+v, %v", invalid, err)
	}

	runtime, root := workspaceRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	if opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-artifact")); err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("open = %+v, %v", opened, err)
	}
	write := proto.Clone(request).(*iop.WorkspaceArtifactRequest)
	write.Operation = iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE
	write.Content = []byte("owned")
	if response, err := n.OnWorkspaceArtifact(context.Background(), nil, write); err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write = %+v, %v", response, err)
	}
	target := filepath.Join(root, ".iop", "job", "request-artifact", "plan.md")
	if err := os.Remove(target); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(target, []byte("raw replacement sentinel"), 0o600); err != nil {
		t.Fatal(err)
	}
	replaced, err := n.OnWorkspaceArtifact(context.Background(), nil, request)
	if err != nil || replaced.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || replaced.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL || strings.Contains(replaced.GetError(), "replacement") || len(replaced.GetContent()) != 0 {
		t.Fatalf("replacement response = %+v, %v", replaced, err)
	}
}

func TestNodeWorkspaceCleanupStableFailuresAndLifecycle(t *testing.T) {
	n, _ := makeNode(t, nil)
	missing, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-1"))
	if err != nil || missing.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED || missing.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
		t.Fatalf("missing=%+v err=%v", missing, err)
	}
	runtime, _ := workspaceRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	opened, _ := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-1"))
	if opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("open=%+v", opened)
	}
	badPath := ".iop/job/request-1/secret"
	result, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{RequestId: "request-1", StageId: "work", ToolCallId: "tool-1", Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, Input: &iop.WorkspaceToolRequest_RelativePath{RelativePath: badPath}})
	if err != nil || result.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST || strings.Contains(result.GetError(), badPath) {
		t.Fatalf("result=%+v err=%v", result, err)
	}
	cancel, _ := n.OnWorkspaceCancel(context.Background(), nil, &iop.WorkspaceCancelRequest{RequestId: "request-1", StageId: "work", ToolCallId: "tool-1"})
	cleanup, _ := n.OnWorkspaceCleanup(context.Background(), nil, &iop.WorkspaceCleanupRequest{RequestId: "request-1"})
	if cancel.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || cancel.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND || cleanup.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || cleanup.GetCleanedArtifacts() != 1 {
		t.Fatalf("cancel=%+v cleanup=%+v", cancel, cleanup)
	}
}

func TestNodeWorkspaceCleanupMappingAndIdempotence(t *testing.T) {
	n, _ := makeNode(t, nil)
	missing, err := n.OnWorkspaceCleanup(context.Background(), nil, &iop.WorkspaceCleanupRequest{RequestId: "request-cleanup"})
	if err != nil || missing.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED || missing.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
		t.Fatalf("missing runtime cleanup = %+v, %v", missing, err)
	}
	invalid, err := n.OnWorkspaceCleanup(context.Background(), nil, nil)
	if err != nil || invalid.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || invalid.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("invalid cleanup = %+v, %v", invalid, err)
	}

	runtime, root := workspaceRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-cleanup"))
	if err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("open = %+v, %v", opened, err)
	}
	if err := runtime.WriteInternalArtifact("request-cleanup", "plan.md", []byte("plan")); err != nil {
		t.Fatal(err)
	}
	request := &iop.WorkspaceCleanupRequest{RequestId: "request-cleanup"}
	first, err := n.OnWorkspaceCleanup(context.Background(), nil, request)
	if err != nil || first.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || first.GetCleanedArtifacts() != 2 {
		t.Fatalf("first cleanup = %+v, %v", first, err)
	}
	second, err := n.OnWorkspaceCleanup(context.Background(), nil, request)
	if err != nil || second.GetStatus() != first.GetStatus() || second.GetErrorCode() != first.GetErrorCode() || second.GetCleanedArtifacts() != first.GetCleanedArtifacts() {
		t.Fatalf("second cleanup = %+v, %v; first=%+v", second, err, first)
	}
	if _, err := os.Stat(filepath.Join(root, ".iop", "job", "request-cleanup")); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("request artifacts remain: %v", err)
	}
	unknown, err := n.OnWorkspaceCleanup(context.Background(), nil, &iop.WorkspaceCleanupRequest{RequestId: "request-unknown"})
	if err != nil || unknown.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND {
		t.Fatalf("unknown cleanup = %+v, %v", unknown, err)
	}
}

func workspaceCommandRuntimeForNode(t *testing.T) (*workspace.Runtime, string) {
	t.Helper()
	root := t.TempDir()
	executable, err := os.Executable()
	if err != nil {
		t.Fatal(err)
	}
	runtime, err := workspace.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=^TestNodeWorkspaceCommandHelperProcess$"},
		}},
		EnvironmentAllowlist: []string{"IOP_NODE_WORKSPACE_HELPER", "IOP_NODE_START_FILE"},
		MaxOutputBytes:       128, MaxCommandTimeoutMs: 5000,
	}}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = runtime.Close() })
	return runtime, root
}

func openNodeCommandWorkspace(t *testing.T, n *nodepkg.Node) {
	t.Helper()
	response, err := n.OnWorkspaceOpen(context.Background(), nil, &iop.WorkspaceOpenRequest{
		RequestId: "request-command", WorkspaceRef: "workspace-command",
		Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
		CommandIds: []string{"helper"}, MaxOutputBytes: 128, MaxCommandTimeoutMs: 5000,
	})
	if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("open command workspace = %+v, %v", response, err)
	}
}

func TestNodeWorkspaceCommand(t *testing.T) {
	n, _ := makeNode(t, nil)
	runtime, _ := workspaceCommandRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	openNodeCommandWorkspace(t, n)

	response, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
		RequestId: "request-command", StageId: "work", ToolCallId: "tool-success",
		Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
		Input:       &iop.WorkspaceToolRequest_CommandId{CommandId: "helper"},
		Environment: map[string]string{"IOP_NODE_WORKSPACE_HELPER": "success"},
	})
	if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || response.GetExitCode() != 0 || string(response.GetStdout()) != "node-command-stdout" || string(response.GetStderr()) != "node-command-stderr" {
		t.Fatalf("command response = %+v, %v", response, err)
	}

	const sentinel = "raw-command-or-environment-sentinel"
	for name, request := range map[string]*iop.WorkspaceToolRequest{
		"unknown-command": {
			RequestId: "request-command", StageId: "work", ToolCallId: "tool-unknown",
			Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
			Input: &iop.WorkspaceToolRequest_CommandId{CommandId: sentinel},
		},
		"unapproved-environment": {
			RequestId: "request-command", StageId: "work", ToolCallId: "tool-environment",
			Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
			Input:       &iop.WorkspaceToolRequest_CommandId{CommandId: "helper"},
			Environment: map[string]string{"HOME": sentinel},
		},
	} {
		t.Run(name, func(t *testing.T) {
			result, callErr := n.OnWorkspaceTool(context.Background(), nil, request)
			if callErr != nil || result.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST || strings.Contains(result.GetError(), sentinel) {
				t.Fatalf("result = %+v, %v", result, callErr)
			}
		})
	}
}

func TestNodeWorkspaceCancel(t *testing.T) {
	n, _ := makeNode(t, nil)
	runtime, root := workspaceCommandRuntimeForNode(t)
	n.SetWorkspaceRuntime(runtime)
	openNodeCommandWorkspace(t, n)
	started := filepath.Join(root, "command-started")
	resultChannel := make(chan *iop.WorkspaceToolResponse, 1)
	go func() {
		response, _ := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
			RequestId: "request-command", StageId: "work", ToolCallId: "tool-cancel",
			Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
			Input:       &iop.WorkspaceToolRequest_CommandId{CommandId: "helper"},
			Environment: map[string]string{"IOP_NODE_WORKSPACE_HELPER": "block", "IOP_NODE_START_FILE": started},
		})
		resultChannel <- response
	}()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if _, err := os.Stat(started); err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if _, err := os.Stat(started); err != nil {
		t.Fatalf("command did not start: %v", err)
	}
	request := &iop.WorkspaceCancelRequest{RequestId: "request-command", StageId: "work", ToolCallId: "tool-cancel"}
	first, err := n.OnWorkspaceCancel(context.Background(), nil, request)
	if err != nil || first.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || first.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
		t.Fatalf("first cancel = %+v, %v", first, err)
	}
	second, err := n.OnWorkspaceCancel(context.Background(), nil, request)
	if err != nil || second.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || second.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
		t.Fatalf("duplicate cancel = %+v, %v", second, err)
	}
	if result := <-resultChannel; result.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || result.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
		t.Fatalf("command result = %+v", result)
	}
	notFound, _ := n.OnWorkspaceCancel(context.Background(), nil, &iop.WorkspaceCancelRequest{RequestId: "request-command", StageId: "work", ToolCallId: "missing"})
	if notFound.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND {
		t.Fatalf("not found cancel = %+v", notFound)
	}
}
