package service

import (
	"encoding/json"
	"errors"
	"strings"
	"testing"

	iop "iop/proto/gen/iop"
)

func internalToolCall(name, arguments string) *InternalWorkspaceToolCall {
	return &InternalWorkspaceToolCall{
		RequestID: "request-1", StageID: "work", ToolCallID: "tool-1",
		Name: name, Arguments: json.RawMessage(arguments),
	}
}

func TestInternalWorkspaceToolDecodeClosedOperations(t *testing.T) {
	tests := []struct {
		name      string
		call      *InternalWorkspaceToolCall
		operation iop.WorkspaceOperation
		check     func(*testing.T, *iop.WorkspaceToolRequest)
	}{
		{"read", internalToolCall(InternalWorkspaceToolRead, `{"relative_path":"README.md"}`), iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, nil},
		{"list", internalToolCall(InternalWorkspaceToolList, `{"relative_path":"."}`), iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST, nil},
		{"delete", internalToolCall(InternalWorkspaceToolDelete, `{"relative_path":"tmp.txt"}`), iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE, nil},
		{"write", internalToolCall(InternalWorkspaceToolWrite, `{"relative_path":"result.txt","content":"done"}`), iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE, func(t *testing.T, req *iop.WorkspaceToolRequest) {
			if req.GetWrite().GetRelativePath() != "result.txt" || string(req.GetWrite().GetContent()) != "done" {
				t.Fatalf("write input = %+v", req.GetWrite())
			}
		}},
		{"command", internalToolCall(InternalWorkspaceToolCommand, `{"command_id":"test","environment":{"IOP_MODE":"safe"}}`), iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, func(t *testing.T, req *iop.WorkspaceToolRequest) {
			if req.GetCommandId() != "test" || req.GetEnvironment()["IOP_MODE"] != "safe" {
				t.Fatalf("command input = %+v", req)
			}
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			req, err := decodeInternalWorkspaceToolCall(test.call)
			if err != nil {
				t.Fatalf("decodeInternalWorkspaceToolCall: %v", err)
			}
			if req.GetRequestId() != "request-1" || req.GetStageId() != "work" || req.GetToolCallId() != "tool-1" || req.GetOperation() != test.operation {
				t.Fatalf("decoded request = %+v", req)
			}
			if test.check != nil {
				test.check(t, req)
			}
		})
	}
}

func TestValidateInternalWorkspaceToolCall(t *testing.T) {
	if err := ValidateInternalWorkspaceToolCall(internalToolCall(InternalWorkspaceToolList, `{"relative_path":"."}`)); err != nil {
		t.Fatalf("canonical call rejected: %v", err)
	}
	if err := ValidateInternalWorkspaceToolCall(internalToolCall(InternalWorkspaceToolRead, `{"relative_path":""}`)); !errors.Is(err, ErrSingleRequestInternalToolInvalidCall) {
		t.Fatalf("empty read error=%v, want invalid call", err)
	}
	if err := ValidateInternalWorkspaceToolCall(nil); !errors.Is(err, ErrSingleRequestInternalToolInvalidCall) {
		t.Fatalf("nil call error=%v, want invalid call", err)
	}
}

func TestInternalWorkspaceToolDecodeRejectsMalformed(t *testing.T) {
	const rawSentinel = "RAW-ARGUMENT-SENTINEL-DO-NOT-LEAK"
	tests := map[string]*InternalWorkspaceToolCall{
		"nil":                    nil,
		"empty request identity": {StageID: "work", ToolCallID: "tool-1", Name: InternalWorkspaceToolRead, Arguments: json.RawMessage(`{"relative_path":"a"}`)},
		"unknown operation":      internalToolCall("shell", `{"command_id":"test"}`),
		"unknown field":          internalToolCall(InternalWorkspaceToolRead, `{"relative_path":"a","secret":"`+rawSentinel+`"}`),
		"trailing value":         internalToolCall(InternalWorkspaceToolRead, `{"relative_path":"a"} {}`),
		"duplicate field":        internalToolCall(InternalWorkspaceToolRead, `{"relative_path":"a","relative_path":"b"}`),
		"absolute path":          internalToolCall(InternalWorkspaceToolRead, `{"relative_path":"/etc/passwd"}`),
		"path traversal":         internalToolCall(InternalWorkspaceToolWrite, `{"relative_path":"../escape","content":"x"}`),
		"private runtime path":   internalToolCall(InternalWorkspaceToolDelete, `{"relative_path":".iop/job/request-1/plan.md"}`),
		"missing write content":  internalToolCall(InternalWorkspaceToolWrite, `{"relative_path":"file"}`),
		"executable injection":   internalToolCall(InternalWorkspaceToolCommand, `{"command_id":"test","executable":"`+rawSentinel+`"}`),
		"argv injection":         internalToolCall(InternalWorkspaceToolCommand, `{"command_id":"test","argv":["sh"]}`),
		"invalid environment":    internalToolCall(InternalWorkspaceToolCommand, `{"command_id":"test","environment":{"1BAD":"value"}}`),
	}
	for name, call := range tests {
		t.Run(name, func(t *testing.T) {
			_, err := decodeInternalWorkspaceToolCall(call)
			if !errors.Is(err, ErrSingleRequestInternalToolInvalidCall) {
				t.Fatalf("error = %v, want invalid call", err)
			}
			if strings.Contains(err.Error(), rawSentinel) {
				t.Fatalf("raw arguments leaked in error %q", err)
			}
		})
	}
}

func TestInternalWorkspaceToolCallCloneAndResultRedaction(t *testing.T) {
	call := internalToolCall(InternalWorkspaceToolWrite, `{"relative_path":"result.txt","content":"private"}`)
	clonedCall := call.Clone()
	call.Arguments[0] = '['
	if string(clonedCall.Arguments) != `{"relative_path":"result.txt","content":"private"}` {
		t.Fatalf("call clone changed through source mutation: %s", clonedCall.Arguments)
	}

	response := &iop.WorkspaceToolResponse{
		RequestId: "request-1", StageId: "work", ToolCallId: "tool-1",
		Status:    iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR,
		ErrorCode: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND,
		Error:     "RAW-NODE-ERROR", Content: []byte("content"), Entries: []string{"entry"},
	}
	result := internalWorkspaceToolResult(response)
	if result.Status != "error" || result.ErrorCode != "not_found" {
		t.Fatalf("typed result = %+v", result)
	}
	encoded, err := json.Marshal(result)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(encoded), response.Error) {
		t.Fatalf("raw Node error leaked in result: %s", encoded)
	}
	clonedResult := result.Clone()
	result.Content[0] = 'X'
	result.Entries[0] = "mutated"
	if string(clonedResult.Content) != "content" || clonedResult.Entries[0] != "entry" {
		t.Fatalf("result clone changed through source mutation: %+v", clonedResult)
	}
}
