package cmd

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/exec"
	"github.com/tidbcloud/AgentTeam/desktop/internal/token"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

func TestExecExitCodeRecognizesRenderedExecErrors(t *testing.T) {
	rendered := execRenderedError{
		envelope: exec.ErrorEnvelope{Code: exec.CodeValidation},
	}
	for _, err := range []error{
		rendered,
		fmt.Errorf("wrapped: %w", rendered),
	} {
		code, ok := ExecExitCode(err)
		require.True(t, ok)
		require.Equal(t, 64, code)
	}
}

func TestExecExitCodePreservesExtensionFailureCodeInJSON(t *testing.T) {
	for _, code := range []exec.ErrorCode{
		exec.CodeRateLimited,
		exec.CodeProviderAuthUnavailable,
		exec.CodeUpstreamFailure,
	} {
		var stderr bytes.Buffer
		command := newExecCommand()
		command.SetErr(&stderr)
		err := renderExecFailure(command, exec.NewError(code, "Extension invocation failed"))
		exitCode, ok := ExecExitCode(err)
		require.True(t, ok)
		require.Equal(t, 69, exitCode)
		require.Contains(t, stderr.String(), `"code":"`+string(code)+`"`)
	}
}

// TestExecExitCodeMapsCorrectedHTTPCategories verifies the corrected typed
// categories for the server's 413/428/429 statuses surface as the right CLI
// exit codes through the full render path: 413 payload-too-large -> validation
// (64), 428 precondition-required -> conflict (1), 429 rate-limited -> 69. The
// status -> category half of the chain is proven in the exec package
// (TestAPIErrorCodeTypes413_428_429); this test proves category -> CLI exit
// code + structured stderr.
func TestExecExitCodeMapsCorrectedHTTPCategories(t *testing.T) {
	cases := []struct {
		name       string
		code       exec.ErrorCode
		serverCode string
		want       int
	}{
		{"413 validation exit 64", exec.CodeValidation, "plan_request_too_large", 64},
		{"428 conflict exit 1", exec.CodeConflict, "precondition_required", 1},
		{"429 rate limited exit 69", exec.CodeRateLimited, "run_request_failed", 69},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := &exec.Error{
				Code:       tc.code,
				Problem:    "workflow operation failed",
				ServerCode: tc.serverCode,
				Cause:      &apiclient.APIError{StatusCode: 400, Message: "upstream detail must stay internal"},
			}
			var stderr bytes.Buffer
			command := newExecCommand()
			command.SetErr(&stderr)
			rendered := renderExecFailure(command, err)
			exitCode, ok := ExecExitCode(rendered)
			require.True(t, ok)
			require.Equal(t, tc.want, exitCode)
			require.Contains(t, stderr.String(), `"code":"`+string(tc.code)+`"`)
			require.Contains(t, stderr.String(), `"serverCode":"`+tc.serverCode+`"`)
			require.NotContains(t, stderr.String(), "upstream detail")
		})
	}
}

// TestExecRenderedErrorNeverLeaksRawCause proves the raw Cause never reaches the
// rendered CLI output even when it carries sensitive detail, and the structured
// stderr carries only the bounded public envelope.
func TestExecRenderedErrorNeverLeaksRawCause(t *testing.T) {
	classified := &exec.Error{
		Code:       exec.CodeValidation,
		Problem:    "workflow plan failed",
		ServerCode: "plan_request_failed",
		Cause:      &apiclient.APIError{StatusCode: 400, Message: "sql: SELECT secret FROM creds", Code: "plan_request_failed"},
	}
	var stderr bytes.Buffer
	command := newExecCommand()
	command.SetErr(&stderr)
	err := renderExecFailure(command, classified)
	require.Error(t, err)
	require.NotContains(t, stderr.String(), "sql:")
	require.NotContains(t, stderr.String(), "SELECT secret")
	require.Contains(t, stderr.String(), `"code":"validation"`)
	require.Contains(t, stderr.String(), `"serverCode":"plan_request_failed"`)
}

func TestExecCobraLocalClientUnavailableDaemonTransportFailsClosed(t *testing.T) {
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	address := listener.Addr().String()
	require.NoError(t, listener.Close())
	setExecCommandTokenFile(t, "http://"+address+"/api/internal/exec/servers/server-1/agents/agent-1", "token")

	_, stderr, execErr := executeCobra(newExecCommand(), "workspace", "list")
	require.Error(t, execErr)
	exitCode, ok := ExecExitCode(execErr)
	require.True(t, ok)
	require.Equal(t, exec.ErrorExitCode(exec.CodeInternal), exitCode)

	var payload struct {
		Error exec.ErrorEnvelope `json:"error"`
	}
	require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(stderr)), &payload))
	assertClosedErrorEnvelope(t, stderr, payload.Error, []string{"code", "problem"})
	var topLevel map[string]json.RawMessage
	require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(stderr)), &topLevel))
	require.Equal(t, []string{"error"}, sortedJSONKeys(topLevel))
	require.Equal(t, exec.CodeInternal, payload.Error.Code)
	require.Equal(t, "internal error", payload.Error.Problem)
	require.Empty(t, payload.Error.ServerCode)
	require.Empty(t, payload.Error.Stage)
	require.Empty(t, payload.Error.Path)
	require.Empty(t, payload.Error.Diagnostics)
	require.False(t, payload.Error.Retryable)
	require.NotContains(t, stderr, address)
	require.NotContains(t, stderr, "127.0.0.1")
	require.NotContains(t, stderr, "token")
}

func TestExecRenderedErrorAppendsHint(t *testing.T) {
	withHint := execRenderedError{envelope: exec.ErrorEnvelope{
		Code:    exec.CodeValidation,
		Problem: "list tasks failed",
		Hint:    "pass channel in #channel[:thread] form",
	}}
	require.Equal(t, "validation: list tasks failed — pass channel in #channel[:thread] form", withHint.Error())

	withoutHint := execRenderedError{envelope: exec.ErrorEnvelope{
		Code:    exec.CodeInternal,
		Problem: "boom",
	}}
	require.Equal(t, "internal: boom", withoutHint.Error())

	codeOnly := execRenderedError{envelope: exec.ErrorEnvelope{Code: exec.CodeNotFound}}
	require.Equal(t, "not_found", codeOnly.Error())
}

func TestExecCommandMutationReceiptSurvivesDaemonHTTPPath(t *testing.T) {
	_, closeDaemon := setExecCommandTestRuntime(t, nil)
	t.Cleanup(closeDaemon)
	daemonBaseURL := os.Getenv(exec.EnvBaseURL)
	tokenPath := os.Getenv(exec.EnvTokenFile)
	tokenBytes, err := os.ReadFile(tokenPath)
	require.NoError(t, err)
	catalogRequest, err := http.NewRequest(http.MethodGet, daemonBaseURL+"/catalog", nil)
	require.NoError(t, err)
	catalogRequest.Header.Set("Authorization", "Bearer "+strings.TrimSpace(string(tokenBytes)))
	catalogResponse, err := http.DefaultClient.Do(catalogRequest)
	require.NoError(t, err)
	catalogBody, err := io.ReadAll(catalogResponse.Body)
	require.NoError(t, catalogResponse.Body.Close())
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, catalogResponse.StatusCode)

	proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasSuffix(r.URL.Path, "/catalog") {
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write(catalogBody)
			return
		}
		require.Equal(t, http.MethodPost, r.Method)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusRequestEntityTooLarge)
		_ = json.NewEncoder(w).Encode(map[string]any{"error": exec.ErrorEnvelope{
			Code: exec.CodeResultBudgetExceeded, Problem: "result too large",
			Hint: "retry is unsafe", Next: "agentteam exec channel update",
			Metadata: map[string]any{
				"mutation_applied": true,
				"mutation_receipt": map[string]any{
					"applied": true, "action": "channel update",
					"identifiers": map[string]string{"channelId": "chn_1"},
					"revision":    "3", "idempotencyKey": "req_1",
					"retrievalPath": `agentteam exec channel get {"channelId":"chn_1"}`,
				},
			},
		}})
	}))
	t.Cleanup(proxy.Close)
	setExecCommandTokenFile(t, proxy.URL+"/api/internal/exec/servers/server-1/agents/agent-1", strings.TrimSpace(string(tokenBytes)))

	_, stderr, err := executeCobra(newExecCommand(), "channel", "update", `{"requestId":"req_1","channelId":"chn_1","expectedVersion":1,"displayName":"ops"}`)
	require.Error(t, err)
	require.Contains(t, stderr, `"mutation_applied":true`)
	require.Contains(t, stderr, "channel get")
	assertMutationAppliedEnvelope(t, stderr)
	require.NotContains(t, strings.ToLower(stderr), "retry")
	require.NotContains(t, strings.ToLower(stderr), "replay")
	exitCode, ok := ExecExitCode(err)
	require.True(t, ok)
	require.Equal(t, 1, exitCode)
}

func TestExecMutationAppliedReceiptDoesNotSuggestReplay(t *testing.T) {
	err := execRenderedError{envelope: exec.ErrorEnvelope{
		Code: exec.CodeResultBudgetExceeded,
		Hint: "Mutation outcome is recorded; inspect the receipt before issuing another command.",
		Metadata: map[string]any{
			"mutation_applied": true,
			"mutation_receipt": map[string]any{"retrievalPath": `agentteam exec skill get {"skillId":"skill_1"}`},
		},
	}}
	require.NotContains(t, strings.ToLower(err.Error()), "retry")
	require.NotContains(t, strings.ToLower(err.Error()), "replay")
	require.Equal(t, 1, err.exitCode())
}

func TestExecCommandThreeLevelHelpAndSchemaOnlyParser(t *testing.T) {
	_, cleanup := setExecCommandTestRuntime(t, nil)
	defer cleanup()
	stdout, stderr, err := executeCobra(newExecCommand(), "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, "agentteam exec <domain> <action> [<json|->]")
	stdout, stderr, err = executeCobra(newExecCommand(), "profile", "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, "set-avatar")
	stdout, stderr, err = executeCobra(newExecCommand(), "profile", "set-avatar", "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, "Example:")
	require.Contains(t, stdout, "--input-schema")
	require.Contains(t, stdout, "--output-schema")
	stdout, stderr, err = executeCobra(newExecCommand(), "profile", "set-avatar", "--input-schema")
	require.NoError(t, err)
	require.Empty(t, stderr)
	var schema map[string]any
	require.NoError(t, json.Unmarshal([]byte(stdout), &schema))
	require.Equal(t, "object", schema["type"])
	require.Contains(t, stdout, `"imagePath"`)
	require.NotContains(t, stdout, `"file_path"`)
	require.NotContains(t, stdout, `"output"`)
	require.NotContains(t, stdout, `"error"`)
	require.NotContains(t, stdout, "Permission:")
	stdout, stderr, err = executeCobra(newExecCommand(), "profile", "set-avatar", "--output-schema")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, `"avatarUrl"`)
	require.NotContains(t, stdout, `"input"`)
	stdout, stderr, err = executeCobra(newExecCommand(), "--error-schema")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, `"problem"`)
	stdout, stderr, err = executeCobra(newExecCommand(), "workflow", "plan", "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, `Example: {"action":"create"`)
	require.Contains(t, stdout, `startThread: true`)
	stdout, stderr, err = executeCobra(newExecCommand(), "workflow", "plan", "--input-schema")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, `"validate"`)
	require.Contains(t, stdout, `"update"`)
	require.Contains(t, stdout, `"publish"`)
	require.NotContains(t, stdout, `"compile"`)
	require.NotContains(t, stdout, `"delete"`)
	stdout, stderr, err = executeCobra(newExecCommand(), "agent", "duplicate", "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, `{"requestId":"req-duplicate-01","sourceAgentId":"agt_123"}`)
	stdout, stderr, err = executeCobra(newExecCommand(), "agent", "update", "--input-schema")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, `"modelProfileId"`)
	require.Contains(t, stdout, `"type": "null"`)
	for _, forbidden := range []string{"channelIds", "apiKey", "baseURL", "headers", "session", "role"} {
		require.NotContains(t, stdout, `"`+forbidden+`"`)
	}
	_, stderr, err = executeCobra(newExecCommand(), "profile", "set-avatar", "--schema")
	require.Error(t, err)
	require.Contains(t, stderr, `"code":"validation"`)
}

func TestExecCommandTransportsSuccessThroughCredentialFile(t *testing.T) {
	gin.SetMode(gin.TestMode)
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/api/v1/workspaces/workspace-1/agents/server-agent-1/runtime/workspace", r.URL.Path)
		require.Equal(t, "summary", r.URL.Query().Get("kind"))
		require.Equal(t, "20", r.URL.Query().Get("limit"))
		require.Equal(t, "Bearer server-agent-token", r.Header.Get("Authorization"))
		_ = json.NewEncoder(w).Encode(map[string]any{
			"kind":     "summary",
			"channels": []any{},
			"agents":   []any{},
			"humans":   []any{},
		})
	}))
	defer upstream.Close()
	keys, err := token.NewKeyManager(t.TempDir() + "/config.yaml")
	require.NoError(t, err)
	authorizations := exec.NewRuntimeAuthorizationStore(keys)
	lease, err := authorizations.Create(exec.RuntimeAuthorizationConfig{
		ServerID:  "server-1",
		AgentID:   "agent-1",
		SessionID: "session-1",
		RunID:     "run-1",
		WorkDir:   t.TempDir(),
		Server: &apiclient.Client{
			ServerURL:        upstream.URL,
			AgentID:          "server-agent-1",
			AuthToken:        "server-agent-token",
			RouteWorkspaceID: "workspace-1",
			HTTPClient:       upstream.Client(),
		},
		TokenTTL: time.Hour,
	})
	require.NoError(t, err)
	r := gin.New()
	exec.RegisterRoutes(r.Group("/api/internal/exec"), exec.RouteDependencies{Authorizations: authorizations})
	srv := httptest.NewServer(r)
	defer srv.Close()
	setExecCommandTokenFile(t, srv.URL+"/api/internal/exec/servers/server-1/agents/agent-1", lease.Token)

	stdout, stderr, err := executeCobra(newExecCommand(), "workspace", "list")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.JSONEq(t, `{
		"text": "Listed workspace.",
		"data": {"kind": "summary", "channels": [], "agents": [], "humans": []}
	}`, strings.TrimSpace(stdout))
}

// TestExecCommandWorkflowActionTransportsTrustedErrorAndExitCode proves the FULL
// real chain for a Workflow action through the CLI: the Cobra CLI -> the local
// exec HTTP handler (RegisterRoutes/handleRuntimeAction) -> the real
// workflowRemote writer -> the real apiclient -> the upstream server emitting the
// canonical Workflow error envelope -> back through the render path -> the typed
// CLI exit code. It asserts the bounded server-approved public message is the
// Problem, the raw details never leak, and the exit code matches the category
// (validation -> 64). The status -> category and category -> exit halves are
// also proven directly, covering 413/428/429.
func TestExecCommandWorkflowActionTransportsTrustedErrorAndExitCode(t *testing.T) {
	gin.SetMode(gin.TestMode)
	const boundedMessage = "bounded server plan message"
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "/api/v1/workspaces/workspace-1/agents/server-agent-1/runtime/workflow/plan", r.URL.Path)
		require.Equal(t, "Bearer server-agent-token", r.Header.Get("Authorization"))
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"error":     boundedMessage,
			"code":      "plan_request_failed",
			"stage":     "plan",
			"path":      "$.input",
			"hint":      "Fix the input.",
			"retryable": false,
			"details": map[string]any{
				"internal_sql": "SELECT secret FROM creds WHERE id = 'tenant_42'",
			},
			"diagnostics": []map[string]any{{
				"severity": "error", "code": "input.unknown_field", "path": "$.input", "message": boundedMessage,
			}},
		})
	}))
	defer upstream.Close()
	keys, err := token.NewKeyManager(t.TempDir() + "/config.yaml")
	require.NoError(t, err)
	authorizations := exec.NewRuntimeAuthorizationStore(keys)
	lease, err := authorizations.Create(exec.RuntimeAuthorizationConfig{
		ServerID: "server-1", AgentID: "agent-1", SessionID: "session-1", RunID: "run-1",
		WorkDir: t.TempDir(),
		Server: &apiclient.Client{
			ServerURL: upstream.URL, LocalAgentID: "agent-1",
			AgentID: "server-agent-1", AuthToken: "server-agent-token",
			RouteWorkspaceID: "workspace-1", HTTPClient: upstream.Client(),
		},
		TokenTTL: time.Hour,
	})
	require.NoError(t, err)
	r := gin.New()
	exec.RegisterRoutes(r.Group("/api/internal/exec"), exec.RouteDependencies{Authorizations: authorizations})
	srv := httptest.NewServer(r)
	defer srv.Close()
	setExecCommandTokenFile(t, srv.URL+"/api/internal/exec/servers/server-1/agents/agent-1", lease.Token)

	stdout, stderr, err := executeCobra(newExecCommand(), "workflow", "plan", `{"action":"show","planId":"pln_1"}`)
	require.Error(t, err)
	require.Empty(t, stdout)
	// The bounded server-approved public message is the Exec Problem; the raw
	// details never leak into the structured stderr.
	require.Contains(t, stderr, `"code":"validation"`)
	require.Contains(t, stderr, `"serverCode":"plan_request_failed"`)
	require.Contains(t, stderr, `"problem":"`+boundedMessage+`"`)
	for _, leaked := range []string{"sql:", "SELECT secret", "tenant_42", "internal_sql"} {
		require.NotContains(t, stderr, leaked, "raw server detail must never reach the CLI stderr")
	}
	// The typed exit code for a validation (413/400) Workflow failure is 64.
	exitCode, ok := ExecExitCode(err)
	require.True(t, ok)
	require.Equal(t, 64, exitCode)
}

// TestExecCommandWorkflowActionNonzeroExitCodesForCorrectedCategories proves the
// corrected 413/428/429 categories surface as nonzero CLI exit codes through the
// real transport and render path.
func TestExecCommandWorkflowActionNonzeroExitCodesForCorrectedCategories(t *testing.T) {
	cases := []struct {
		name     string
		status   int
		code     string
		wantExit int
		wantCat  exec.ErrorCode
	}{
		{"413 payload too large -> validation 64", http.StatusRequestEntityTooLarge, "plan_request_too_large", 64, exec.CodeValidation},
		{"428 precondition required -> conflict 1", http.StatusPreconditionRequired, "precondition_required", 1, exec.CodeConflict},
		{"429 rate limited -> 69", http.StatusTooManyRequests, "run_request_failed", 69, exec.CodeRateLimited},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			gin.SetMode(gin.TestMode)
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(tc.status)
				_ = json.NewEncoder(w).Encode(map[string]any{"error": "bounded message", "code": tc.code})
			}))
			defer upstream.Close()
			keys, err := token.NewKeyManager(t.TempDir() + "/config.yaml")
			require.NoError(t, err)
			authorizations := exec.NewRuntimeAuthorizationStore(keys)
			lease, err := authorizations.Create(exec.RuntimeAuthorizationConfig{
				ServerID: "server-1", AgentID: "agent-1", SessionID: "session-1", RunID: "run-1",
				WorkDir: t.TempDir(),
				Server: &apiclient.Client{
					ServerURL: upstream.URL, LocalAgentID: "agent-1",
					AgentID: "server-agent-1", AuthToken: "server-agent-token",
					RouteWorkspaceID: "workspace-1", HTTPClient: upstream.Client(),
				},
				TokenTTL: time.Hour,
			})
			require.NoError(t, err)
			r := gin.New()
			exec.RegisterRoutes(r.Group("/api/internal/exec"), exec.RouteDependencies{Authorizations: authorizations})
			srv := httptest.NewServer(r)
			defer srv.Close()
			setExecCommandTokenFile(t, srv.URL+"/api/internal/exec/servers/server-1/agents/agent-1", lease.Token)

			_, stderr, err := executeCobra(newExecCommand(), "workflow", "plan", `{"action":"show","planId":"pln_1"}`)
			require.Error(t, err)
			require.Contains(t, stderr, `"code":"`+string(tc.wantCat)+`"`)
			exitCode, ok := ExecExitCode(err)
			require.True(t, ok)
			require.Equal(t, tc.wantExit, exitCode)
		})
	}
}

func TestExecCommandReadsCurrentTokenFileOnEveryInvocation(t *testing.T) {
	gin.SetMode(gin.TestMode)
	keys, err := token.NewKeyManager(t.TempDir() + "/config.yaml")
	require.NoError(t, err)
	authorizations := exec.NewRuntimeAuthorizationStore(keys)
	lease, err := authorizations.Create(exec.RuntimeAuthorizationConfig{
		ServerID: "server-1", AgentID: "agent-1", SessionID: "session-1", RunID: "run-1",
		WorkDir: t.TempDir(), Server: &apiclient.Client{}, TokenTTL: time.Hour,
	})
	require.NoError(t, err)
	router := gin.New()
	exec.RegisterRoutes(
		router.Group("/api/internal/exec"),
		exec.RouteDependencies{Authorizations: authorizations},
	)
	server := httptest.NewServer(router)
	defer server.Close()

	tokenFile := filepath.Join(t.TempDir(), "exec-token")
	require.NoError(t, os.WriteFile(tokenFile, []byte(lease.Token+"\n"), 0o600))
	setExecCommandTokenFileEnv(
		t,
		server.URL+"/api/internal/exec/servers/server-1/agents/agent-1",
		tokenFile,
		"legacy-token-must-not-be-used",
	)

	stdout, stderr, err := executeCobra(newExecCommand(), "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, "Built-in actions")

	rotated, err := authorizations.Rotate(lease.AuthorizationID)
	require.NoError(t, err)
	replacement := tokenFile + ".next"
	require.NoError(t, os.WriteFile(replacement, []byte(rotated.Token+"\n"), 0o600))
	require.NoError(t, os.Rename(replacement, tokenFile))
	require.True(t, authorizations.RetireCredential(lease.AuthorizationID, lease.CredentialID))

	stdout, stderr, err = executeCobra(newExecCommand(), "--help")
	require.NoError(t, err)
	require.Empty(t, stderr)
	require.Contains(t, stdout, "Built-in actions")
}

func TestExecCommandTokenFileFailuresFailClosed(t *testing.T) {
	tests := []struct {
		name  string
		write func(*testing.T, string)
	}{
		{name: "missing"},
		{
			name: "empty",
			write: func(t *testing.T, path string) {
				require.NoError(t, os.WriteFile(path, []byte(" \n\t"), 0o600))
			},
		},
		{
			name: "oversize",
			write: func(t *testing.T, path string) {
				require.NoError(t, os.WriteFile(path, bytes.Repeat([]byte{'x'}, int(maxExecTokenFileBytes)+1), 0o600))
			},
		},
		{
			name: "control character",
			write: func(t *testing.T, path string) {
				require.NoError(t, os.WriteFile(path, []byte("file-token\x00suffix\n"), 0o600))
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var calls atomic.Int32
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				calls.Add(1)
				w.Header().Set("Content-Type", "application/json")
				_, _ = w.Write([]byte(`{"data":{},"text":"unexpected"}`))
			}))
			defer server.Close()

			tokenFile := filepath.Join(t.TempDir(), "exec-token")
			if test.write != nil {
				test.write(t, tokenFile)
			}
			setExecCommandTokenFileEnv(
				t,
				server.URL+"/api/internal/exec/servers/server-1/agents/agent-1",
				tokenFile,
				"legacy-token-must-not-be-used",
			)

			stdout, stderr, err := executeCobra(newExecCommand(), "workspace", "list")
			require.Error(t, err)
			require.Empty(t, stdout)
			require.Contains(t, stderr, `"code":"permission_denied"`)
			require.Contains(t, stderr, `"problem":"Exec runtime environment is unavailable"`)
			require.NotContains(t, stderr, tokenFile)
			require.NotContains(t, stderr, "legacy-token")
			require.Zero(t, calls.Load())
		})
	}
}

func TestExecCommandRejectsMissingLaunchAuthorityBeforeReadingStdin(t *testing.T) {
	t.Setenv(exec.EnvBaseURL, "")
	t.Setenv(exec.EnvTokenFile, "")
	var reads atomic.Int32
	stdin := readerFunc(func([]byte) (int, error) {
		reads.Add(1)
		return 0, errors.New("stdin must not be read")
	})
	cmd := newExecCommand()
	cmd.SetIn(stdin)

	_, stderr, err := executeCobra(cmd, "workflow", "plan", "-")

	require.Error(t, err)
	require.Contains(t, stderr, `"code":"permission_denied"`)
	require.Zero(t, reads.Load())
}

func TestResolveExecRuntimeTokenRejectsRelativeSymlinkAndBroadPermissions(t *testing.T) {
	t.Run("relative path", func(t *testing.T) {
		_, err := resolveExecRuntimeToken(func(key string) string {
			if key == exec.EnvTokenFile {
				return "relative/token"
			}
			return "legacy-must-not-be-used"
		})
		require.Error(t, err)
	})

	if runtime.GOOS != "windows" {
		t.Run("symlink", func(t *testing.T) {
			dir := t.TempDir()
			target := filepath.Join(dir, "target")
			link := filepath.Join(dir, "token")
			require.NoError(t, os.WriteFile(target, []byte("file-token\n"), 0o600))
			require.NoError(t, os.Symlink(target, link))
			_, err := resolveExecRuntimeToken(func(key string) string {
				if key == exec.EnvTokenFile {
					return link
				}
				return "legacy-must-not-be-used"
			})
			require.Error(t, err)
		})

		t.Run("broad permissions", func(t *testing.T) {
			path := filepath.Join(t.TempDir(), "token")
			require.NoError(t, os.WriteFile(path, []byte("file-token\n"), 0o644))
			_, err := resolveExecRuntimeToken(func(key string) string {
				if key == exec.EnvTokenFile {
					return path
				}
				return "legacy-must-not-be-used"
			})
			require.Error(t, err)
		})
	}
}

func TestReadExecRuntimeTokenFileRetriesAtomicRenameBetweenLstatAndOpen(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "token")
	replacement := filepath.Join(dir, "replacement")
	require.NoError(t, os.WriteFile(path, []byte("old-token\n"), 0o600))
	require.NoError(t, os.WriteFile(replacement, []byte("new-token\n"), 0o600))

	first := true
	lstat := func(name string) (os.FileInfo, error) {
		info, err := os.Lstat(name)
		if err != nil || !first {
			return info, err
		}
		first = false
		// Return the old inode after atomically publishing the new inode. The
		// first Open must mismatch; the bounded second attempt must succeed.
		if runtime.GOOS == "windows" {
			require.NoError(t, os.Remove(path))
		}
		require.NoError(t, os.Rename(replacement, path))
		return info, nil
	}

	value, err := readExecRuntimeTokenFile(path, lstat, os.Open)
	require.NoError(t, err)
	require.Equal(t, "new-token", value)
}

func TestExecCommandTransportsTypedBoundedErrorThroughActionEndpoint(t *testing.T) {
	_, cleanup := setExecCommandTestRuntime(t, nil)
	defer cleanup()

	stdout, stderr, err := executeCobra(newExecCommand(), "workspace", "list")
	require.Error(t, err)
	require.Empty(t, stdout)
	require.Contains(t, stderr, `"code":"internal"`)
	require.NotContains(t, stderr, "cause")
	require.NotContains(t, stderr, "internal:")
	require.Less(t, len(stderr), 64*1024)
}

func TestReadExecInputInlineAndStdin(t *testing.T) {
	inline, hash, size, err := readExecInput(bytes.NewBuffer(nil), `{"id":"x"}`)
	require.NoError(t, err)
	require.JSONEq(t, `{"id":"x"}`, string(inline))
	require.Empty(t, hash)
	require.Zero(t, size)
	fromStdin, hash, size, err := readExecInput(bytes.NewBufferString("{\"id\":\"x\"}"), "-")
	require.NoError(t, err)
	require.JSONEq(t, `{"id":"x"}`, string(fromStdin))
	require.NotEmpty(t, hash)
	require.Equal(t, int64(10), size)
	_, _, _, err = readExecInput(bytes.NewReader(bytes.Repeat([]byte{'x'}, int(maxExecInputJSONBytes)+1)), "-")
	require.EqualError(t, err, "input-json exceeds 1 MiB")
	_, _, _, err = readExecInput(bytes.NewBuffer(nil), strings.Repeat("x", int(maxExecInputJSONBytes)+1))
	require.EqualError(t, err, "input-json exceeds 1 MiB")
}

func TestExecCommandRejectsWorkflowDuplicateKeysBeforeHTTP(t *testing.T) {
	_, cleanup := setExecCommandTestRuntime(t, nil)
	defer cleanup()
	cmd := newExecCommand()
	cmd.SetArgs([]string{"workflow", "plan", `{"action":"get","action":"list","planId":"wpl_1"}`})
	var stdout, stderr bytes.Buffer
	cmd.SetOut(&stdout)
	cmd.SetErr(&stderr)
	err := cmd.Execute()
	require.Error(t, err)
	require.Empty(t, stdout.String())
	require.Contains(t, stderr.String(), `"code":"validation"`)
	require.NotContains(t, stderr.String(), "duplicate JSON field")
}

func TestExecCommandAvailableOnlyWithBaseURLAndAbsoluteCredentialFile(t *testing.T) {
	const baseURL = "http://127.0.0.1:43123/api/internal/exec/servers/server-1/agents/agent-1"
	require.False(t, execRuntimeEnvAvailable(func(string) string { return "" }))
	require.False(t, execRuntimeEnvAvailable(func(key string) string {
		if key == exec.EnvBaseURL {
			return baseURL
		}
		return ""
	}))
	require.False(t, execRuntimeEnvAvailable(func(key string) string {
		switch key {
		case exec.EnvBaseURL:
			return baseURL
		case "AGENTTEAM_EXEC_TOKEN":
			return "retired-raw-token"
		}
		return ""
	}))
	require.True(t, execRuntimeEnvAvailable(func(key string) string {
		switch key {
		case exec.EnvBaseURL:
			return baseURL
		case exec.EnvTokenFile:
			return "/declared/even-if-currently-unreadable"
		}
		return ""
	}))
	require.False(t, execRuntimeEnvAvailable(func(key string) string {
		switch key {
		case exec.EnvBaseURL:
			return baseURL
		case exec.EnvTokenFile:
			return "relative/token"
		}
		return ""
	}))
}

func TestPrivateRuntimeRegistrationExposesOnlyExec(t *testing.T) {
	root := &cobra.Command{Use: "agentteam"}
	registerPrivateRuntimeCommands(
		root,
		func(key string) string {
			switch key {
			case exec.EnvBaseURL:
				return "http://127.0.0.1:43123/api/internal/exec/servers/server-1/agents/agent-1"
			case exec.EnvTokenFile:
				return "/private/token"
			default:
				return ""
			}
		},
		newExecCommand(),
	)
	names := make([]string, 0, len(root.Commands()))
	for _, command := range root.Commands() {
		names = append(names, command.Name())
	}
	require.Equal(t, []string{"exec"}, names)
}

func setExecCommandTokenFile(t *testing.T, baseURL, token string) {
	t.Helper()
	tokenFile := filepath.Join(t.TempDir(), "exec-token")
	require.NoError(t, os.WriteFile(tokenFile, []byte(token+"\n"), 0o600))
	setExecCommandTokenFileEnv(t, baseURL, tokenFile, "retired-raw-token")
}

func setExecCommandTokenFileEnv(t *testing.T, baseURL, tokenFile, legacyToken string) {
	t.Helper()
	t.Setenv(exec.EnvBaseURL, baseURL)
	t.Setenv(exec.EnvTokenFile, tokenFile)
	t.Setenv("AGENTTEAM_EXEC_TOKEN", legacyToken)
}

func setExecCommandTestRuntime(t *testing.T, runtimeCatalog *exec.RuntimeCatalog) (*exec.RuntimeAuthorizationStore, func()) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	keys, err := token.NewKeyManager(t.TempDir() + "/config.yaml")
	require.NoError(t, err)
	authorizations := exec.NewRuntimeAuthorizationStore(keys)
	lease, err := authorizations.Create(exec.RuntimeAuthorizationConfig{
		ServerID: "server-1", AgentID: "agent-1", SessionID: "session-1", RunID: "run-1",
		WorkDir: t.TempDir(), RuntimeCatalog: runtimeCatalog, Server: &apiclient.Client{}, TokenTTL: time.Hour,
	})
	require.NoError(t, err)
	router := gin.New()
	exec.RegisterRoutes(router.Group("/api/internal/exec"), exec.RouteDependencies{Authorizations: authorizations})
	server := httptest.NewServer(router)
	setExecCommandTokenFile(t, server.URL+"/api/internal/exec/servers/server-1/agents/agent-1", lease.Token)
	return authorizations, server.Close
}

func executeCobra(cmd *cobra.Command, args ...string) (string, string, error) {
	var stdout, stderr bytes.Buffer
	cmd.SetOut(&stdout)
	cmd.SetErr(&stderr)
	cmd.SetArgs(args)
	err := cmd.Execute()
	return stdout.String(), stderr.String(), err
}

type readerFunc func([]byte) (int, error)

func (read readerFunc) Read(buffer []byte) (int, error) {
	return read(buffer)
}
