package run

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	store "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
	"github.com/tidbcloud/AgentTeam/internal/workflow/runmanage"
)

// TestRunEffectActivationProjectionPreservesPublicFailureAndRedaction
// verifies that the model-facing Run/effect/activation/log projections carry the
// public-safe failure payload (code/stage/path/problem/hint/retryable
// plus safe diagnostics and workItem context) and still redact operational keys
// and secret-bearing diagnostics that are never authored into the public
// shape.
func TestRunEffectActivationProjectionPreservesPublicFailureAndRedaction(t *testing.T) {
	now := time.Now().UTC()
	publicError := json.RawMessage(`{
		"code":"message.delivery_failed",
		"stage":"dispatch",
		"path":"notify",
		"problem":"The message could not be delivered",
		"hint":"The message could not be delivered",
		"retryable":true,
		"diagnostics":[{"code":"message.delivery_failed","path":"notify","field":"target","message":"safe diagnostic"}],
		"workItem":{"key":"child","index":1,"dependencyPath":"dep"}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: publicError,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	require.Equal(t, "message.delivery_failed", response.ErrorCode)
	require.JSONEq(t, string(publicError), string(response.Error))
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	require.NotContains(t, string(raw), "leaseToken")
	require.NotContains(t, string(raw), "password")

	activation := store.Activation{
		ID: "wra_6b86b273ff34fce19d6b804eff5a3f57", RunID: run.ID, NodeID: "notify", Opcode: "send_message",
		Status: store.ActivationStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: publicError,
		ScopeContextJSON: json.RawMessage(`{}`), InputJSON: json.RawMessage(`{}`),
		CreatedAt: now, UpdatedAt: now,
	}
	activationResponse := ActivationResponse(activation)
	require.Equal(t, "message.delivery_failed", activationResponse.ErrorCode)
	require.JSONEq(t, string(publicError), string(activationResponse.Error))
	raw, err = json.Marshal(activationResponse)
	require.NoError(t, err)
	require.NotContains(t, string(raw), "leaseOwner")

	effect := store.Effect{
		ID: "wfx_baa86a9e8b16589a778ee2cfce0bef62", RunID: run.ID, ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindMessage,
		Status: store.EffectStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: publicError,
		IdempotencyKey: "idempotency_secret", RequestJSON: json.RawMessage(`{"leaseToken":"secret"}`),
		CreatedAt: now, UpdatedAt: now,
	}
	effectResponse := EffectResponse(effect)
	require.Equal(t, "message.delivery_failed", effectResponse.ErrorCode)
	require.JSONEq(t, string(publicError), string(effectResponse.Error))
	raw, err = json.Marshal(effectResponse)
	require.NoError(t, err)
	require.NotContains(t, string(raw), "idempotency_secret")
	require.NotContains(t, string(raw), "secret")

	// The Run log projection carries the same public failure payload and
	// redacts operational keys never authored into the public shape.
	event := store.Event{
		ID: "wre_1", RunID: run.ID, Seq: 1, Type: "run_failed",
		PayloadJSON: publicError, CreatedAt: now,
	}
	logResponse := RunLogResponse(event)
	require.Equal(t, "error", logResponse.Level)
	require.Equal(t, "Run failed", logResponse.Message)
	require.JSONEq(t, string(publicError), string(logResponse.Details))
	raw, err = json.Marshal(logResponse)
	require.NoError(t, err)
	require.NotContains(t, string(raw), "leaseToken")
	require.NotContains(t, string(raw), "password")

	// Operational keys nested inside a public payload are still redacted,
	// while the safe public fields survive.
	withOperational := json.RawMessage(`{
		"code":"approval.delivery_failed",
		"stage":"approval",
		"problem":"The approval could not be delivered",
		"hint":"The approval could not be delivered",
		"leaseToken":"lease_secret",
		"executionIdentity":{"ownerUserId":"usr_00000000secret"}
	}`)
	effect2 := store.Effect{
		ID: "wfx_1d188c4160a2a281bf937d0c7fc94a81", RunID: run.ID, ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindApproval,
		Status: store.EffectStatusFailed, ErrorCode: "approval.delivery_failed", ErrorJSON: withOperational,
		CreatedAt: now, UpdatedAt: now,
	}
	effect2Response := EffectResponse(effect2)
	raw, err = json.Marshal(effect2Response)
	require.NoError(t, err)
	require.NotContains(t, string(raw), "lease_secret")
	require.NotContains(t, string(raw), "usr_00000000secret")
	require.Contains(t, string(raw), "The approval could not be delivered")
}

// TestStateJSONProjectionPreservesUserAuthoredFailureKeys verifies Run show
// State preserves arbitrary user-authored variables/input/output that happen to
// be named `failure`, `failureCode`, or `failureMessage` at nested map/array
// locations, while operational keys are redacted. Kernel failure objects are
// inherently safe at write time (#t2617), so the projection does not strip by
// key name.
func TestStateJSONProjectionPreservesUserAuthoredFailureKeys(t *testing.T) {
	now := time.Now().UTC()
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{"code":"run.graph_deadlocked","stage":"reducer","problem":"The workflow graph is deadlocked","hint":"The workflow graph is deadlocked"},
		"variables":{
			"failure":"user value",
			"nested":{"failureCode":"user_code","failureMessage":"user message"}
		},
		"input":{"failure":"input value"},
		"activations":[{"id":"a1","failure":{"code":"message.target_invalid","stage":"reducer"}}],
		"leaseToken":"lease_secret",
		"executionIdentity":{"ownerUserId":"usr_00000000secret"}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "run.graph_deadlocked",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	resp := RunDetailResponse(run)
	raw, err := json.Marshal(resp)
	require.NoError(t, err)
	// Arbitrary user-authored values named failure/failureCode/failureMessage
	// survive in variables/input.
	require.Contains(t, string(raw), "user value")
	require.Contains(t, string(raw), "user_code")
	require.Contains(t, string(raw), "user message")
	require.Contains(t, string(raw), "input value")
	// Public kernel failure fields survive.
	require.Contains(t, string(raw), "run.graph_deadlocked")
	require.Contains(t, string(raw), "message.target_invalid")
	// Operational keys are redacted.
	require.NotContains(t, string(raw), "lease_secret")
	require.NotContains(t, string(raw), "usr_00000000secret")
}

// TestEffectResultProjectionPreservesUserOutput verifies effect Result
// projection preserves the public failure fields and arbitrary user output,
// and redacts operational keys.
func TestEffectResultProjectionPreservesUserOutput(t *testing.T) {
	now := time.Now().UTC()
	resultJSON := json.RawMessage(`{
		"port":"failed",
		"failureCode":"agent.failed",
		"failureMessage":"Workflow Run failed",
		"output":{"failure":"user output","failureCode":"user_code"},
		"idempotencyKey":"idem_secret"
	}`)
	effect := store.Effect{
		ID: "wfx_baa86a9e8b16589a778ee2cfce0bef62", RunID: "wri_1", ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindMessage,
		Status: store.EffectStatusFailed, ResultJSON: resultJSON,
		CreatedAt: now, UpdatedAt: now,
	}
	resp := EffectResponse(effect)
	raw, err := json.Marshal(resp)
	require.NoError(t, err)
	require.Contains(t, string(raw), "agent.failed")
	require.Contains(t, string(raw), "Workflow Run failed")
	require.Contains(t, string(raw), "user output")
	require.Contains(t, string(raw), "user_code")
	require.NotContains(t, string(raw), "idem_secret")
}

// TestLegacyErrorJSONProjectionDropsArbitraryMessage proves a legacy
// {code, message} ErrorJSON through the Run/effect/activation Error projections
// never retains the arbitrary authoring message. A known code derives the
// bounded safe problem/hint; unknown prose fails closed to the generic bounded
// problem; structured stage/path/retryable/diagnostics/workItem survive.
func TestLegacyErrorJSONProjectionDropsArbitraryMessage(t *testing.T) {
	now := time.Now().UTC()

	knownLegacy := json.RawMessage(`{
		"code":"message.delivery_failed",
		"message":"SELECT * FROM users WHERE password='hunter2' AND tenant='acme' token=secret stack=at main.go:42"
	}`)
	unknownLegacy := json.RawMessage(`{
		"code":"legacy.unreviewed_prose",
		"message":"arbitrary cause password=hunter2 tenant=acme SELECT * FROM x"
	}`)

	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: knownLegacy,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	runResponse := RunDetailResponse(run)
	runRaw, err := json.Marshal(runResponse)
	require.NoError(t, err)
	runText := string(runRaw)
	for _, secret := range []string{"hunter2", "password", "tenant=acme", "SELECT", "token=secret", "main.go", "arbitrary"} {
		require.NotContains(t, runText, secret)
	}
	// Known code derives the bounded public problem/hint.
	require.Contains(t, runText, "The message could not be delivered")
	require.NotContains(t, runText, `"message":`)

	activation := store.Activation{
		ID: "wra_6b86b273ff34fce19d6b804eff5a3f57", RunID: run.ID, NodeID: "notify", Opcode: "send_message",
		Status: store.ActivationStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: unknownLegacy,
		ScopeContextJSON: json.RawMessage(`{}`), InputJSON: json.RawMessage(`{}`),
		CreatedAt: now, UpdatedAt: now,
	}
	activationResponse := ActivationResponse(activation)
	activationRaw, err := json.Marshal(activationResponse)
	require.NoError(t, err)
	activationText := string(activationRaw)
	for _, secret := range []string{"hunter2", "password", "tenant=acme", "SELECT", "arbitrary"} {
		require.NotContains(t, activationText, secret)
	}
	// Unknown prose fails closed to the generic public problem.
	require.Contains(t, activationText, "Workflow Run failed")
	require.NotContains(t, activationText, `"message":`)

	effect := store.Effect{
		ID: "wfx_baa86a9e8b16589a778ee2cfce0bef62", RunID: run.ID, ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindMessage,
		Status: store.EffectStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: knownLegacy,
		CreatedAt: now, UpdatedAt: now,
	}
	effectResponse := EffectResponse(effect)
	effectRaw, err := json.Marshal(effectResponse)
	require.NoError(t, err)
	effectText := string(effectRaw)
	for _, secret := range []string{"hunter2", "password", "tenant=acme", "SELECT", "token=secret", "main.go"} {
		require.NotContains(t, effectText, secret)
	}
	require.Contains(t, effectText, "The message could not be delivered")
	require.NotContains(t, effectText, `"message":`)
}

// TestLegacyErrorJSONProjectionRetainsStructuredFields proves a legacy
// {code, message} ErrorJSON that also carries safe structured fields keeps
// stage/path/retryable/diagnostics/workItem while the message is projected away.
func TestLegacyErrorJSONProjectionRetainsStructuredFields(t *testing.T) {
	now := time.Now().UTC()
	legacy := json.RawMessage(`{
		"code":"message.target_invalid",
		"message":"SELECT password FROM users WHERE tenant='acme'",
		"stage":"reducer",
		"path":"notify",
		"retryable":false,
		"diagnostics":[{"code":"message.target_invalid","path":"notify","field":"target","message":"safe diagnostic"}],
		"workItem":{"key":"child","index":0}
	}`)
	effect := store.Effect{
		ID: "wfx_baa86a9e8b16589a778ee2cfce0bef62", RunID: "wri_1", ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindMessage,
		Status: store.EffectStatusFailed, ErrorCode: "message.target_invalid", ErrorJSON: legacy,
		CreatedAt: now, UpdatedAt: now,
	}
	response := EffectResponse(effect)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	require.NotContains(t, text, "hunter2")
	require.NotContains(t, text, "SELECT password")
	require.NotContains(t, text, "tenant=acme")
	// The arbitrary authoring message text is never projected.
	require.NotContains(t, text, "SELECT password FROM users")
	// Structured safe fields survive.
	require.Contains(t, text, "message.target_invalid")
	require.Contains(t, text, `"stage":"reducer"`)
	require.Contains(t, text, `"path":"notify"`)
	require.Contains(t, text, `"retryable":false`)
	require.Contains(t, text, `"diagnostics"`)
	require.Contains(t, text, `"workItem"`)
	require.Contains(t, text, `"key":"child"`)
	// Public problem derived from the known code.
	require.Contains(t, text, "The message target is invalid")
}

// TestLegacyErrorJSONReachesHTTPWireWithPublicFailure proves a legacy
// {code, message} ErrorJSON with historical SQL/password/token/tenant/stack
// payloads, served through the real Run HTTP handler (the same response the
// Agent API, Desktop apiclient, and CLI consume), reaches the wire with the
// arbitrary message projected away and the bounded safe problem/hint in its
// place.
func TestLegacyErrorJSONReachesHTTPWireWithPublicFailure(t *testing.T) {
	gin.SetMode(gin.TestMode)
	now := time.Now().UTC()
	legacy := json.RawMessage(`{
		"code":"message.delivery_failed",
		"message":"SELECT * FROM users WHERE password='hunter2' AND tenant='acme' token=sec stack=at main.go:42"
	}`)
	fake := &fakeService{run: store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.delivery_failed", ErrorJSON: legacy,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}}
	handler := NewHandler(fake)

	router := gin.New()
	router.GET("/workflow/runs/:runId",
		func(ctx *gin.Context) {
			api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_00000000000001"}})
			api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1", MemberID: "000000000001", MemberUserID: "usr_00000000000001", WorkspaceRole: "owner"})
		},
		handleWorkspace(handler.Get),
	)
	request := httptest.NewRequest(http.MethodGet, "/workflow/runs/wri_1", http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
	body := recorder.Body.String()
	for _, secret := range []string{"hunter2", "password", "tenant=acme", "SELECT", "token=sec", "main.go"} {
		require.NotContains(t, body, secret)
	}
	// The bounded public problem/hint replace the arbitrary authoring text.
	require.Contains(t, body, "The message could not be delivered")
	require.NotContains(t, body, "SELECT * FROM users")
}

// TestRunLogEffectTimedOutDetailsCarriesSafeContract verifies the
// effect_timed_out event produced by the real dispatcher resolves through
// RunLogResponse.Details carrying the public safe failure contract
// (code/stage/path/problem/hint/retryable) and redacting operational keys.
func TestRunLogEffectTimedOutDetailsCarriesSafeContract(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_t", RunID: "wri_1", Seq: 1, Type: "effect_timed_out",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_038fc924dd87bec4ad039322818f92cc","activationId":"a00000000000000000002","nodeId":"wait",
			"code":"effect.timed_out","stage":"dispatch","path":"wait",
			"problem":"The workflow effect timed out","hint":"The workflow effect timed out",
			"retryable":true,
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	require.Equal(t, "info", log.Level, "effect_timed_out is a timeout notice, not a failed event")
	require.Equal(t, "Effect timed out", log.Message)
	require.Contains(t, string(log.Details), "effect.timed_out")
	require.Contains(t, string(log.Details), `"stage":"dispatch"`)
	require.Contains(t, string(log.Details), `"path":"wait"`)
	require.Contains(t, string(log.Details), `"retryable":true`)
	require.Contains(t, string(log.Details), "The workflow effect timed out")
	require.NotContains(t, string(log.Details), "leaseToken")
	require.NotContains(t, string(log.Details), "lease_secret")
}

// TestStateSchemaKnownFailureProjection proves historical nested
// message/failureMessage inside typed kernel State failure locations are
// projected into safe bounded code→problem/hint contracts (unknown prose
// fails closed), while arbitrary user input/variables/output survive exactly
// even when their keys collide with failure, failureCode, failureMessage, or
// operational-key names such as ownerUserId/clientMessageId.
func TestStateSchemaKnownFailureProjection(t *testing.T) {
	now := time.Now().UTC()
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{"code":"run.graph_deadlocked","message":"SELECT * FROM users WHERE password='hunter2'","stage":"reducer"},
		"variables":{
			"failure":"user value",
			"ownerUserId":"usr_0000000000user_variable",
			"clientMessageId":"usr_00000000client_message",
			"nested":{"failureMessage":"user nested message","leaseToken":"user_lease"}
		},
		"input":{"failureCode":"user_input_code"},
		"output":{"ownerMemberId":"usr_0000000000user_output"},
		"activations":[
			{"id":"a1","failure":{"code":"message.target_invalid","message":"SELECT password FROM tenants WHERE tenant_id='acme'","stage":"reducer"}},
			{"id":"a2","input":{"clientMessageId":"usr_0000activation_input"}}
		],
		"scopes":[
			{"id":"s1","parallel":{"branches":[{"port":"p","failure":{"code":"message.responder_invalid","message":"token=secret stack=at main.go:42"}}]}},
			{"id":"s2","fanOut":{"items":[{"key":"child","failure":{"code":"work_item.dependency_blocked","message":"blocked by password='hunter2'"}}]}}
		]
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "run.graph_deadlocked",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)

	// Historical nested message text never survives; the stable problem/hint
	// replaces it.
	for _, secret := range []string{"hunter2", "password", "tenant_id", "acme", "SELECT", "token=secret", "main.go"} {
		require.NotContains(t, text, secret)
	}
	// Unknown legacy prose fails closed to the generic public problem.
	require.Contains(t, text, "The workflow graph is deadlocked")
	require.Contains(t, text, "The message target is invalid")
	require.Contains(t, text, "The message responder is invalid")
	require.Contains(t, text, "A work item is blocked by an unsuccessful dependency")

	// User-authored values (including collision keys) survive exactly.
	require.Contains(t, text, "user value")
	require.Contains(t, text, "usr_0000000000user_variable")
	require.Contains(t, text, "usr_00000000client_message")
	require.Contains(t, text, "user nested message")
	require.Contains(t, text, "user_lease")
	require.Contains(t, text, "user_input_code")
	require.Contains(t, text, "usr_0000000000user_output")
	require.Contains(t, text, "usr_0000activation_input")
	// Raw message/failureMessage keys are never projected from the typed failures.
	require.NotContains(t, text, `"message":"SELECT`)
	require.NotContains(t, text, `"message":"blocked`)
	require.NotContains(t, text, `"message":"token`)
}

// TestStateUnknownLegacyFailureProseFailsClosed proves a schema-known State
// failure carrying an unknown code and raw prose fails closed to the generic
// bounded problem instead of leaking the prose.
func TestStateUnknownLegacyFailureProseFailsClosed(t *testing.T) {
	now := time.Now().UTC()
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{"code":"legacy.unreviewed_code","message":"arbitrary cause password=hunter2 tenant=acme"},
		"input":{}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "legacy.unreviewed_code",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	for _, secret := range []string{"hunter2", "password", "tenant=acme", "arbitrary"} {
		require.NotContains(t, text, secret)
	}
	// Unknown code fails closed to the generic bounded problem.
	require.Contains(t, text, "Workflow Run failed")
	require.NotContains(t, text, `"message":`)
}

// TestEffectResultSchemaKnownFailureMessageProjection proves a historical
// failureMessage in a durable EffectResult is projected into the safe
// bounded code→problem/hint contract (unknown prose fails closed) while the
// user-authored output survives exactly even when it collides with operational
// and failure-key names.
func TestEffectResultSchemaKnownFailureMessageProjection(t *testing.T) {
	now := time.Now().UTC()
	resultJSON := json.RawMessage(`{
		"port":"failed",
		"failureCode":"agent.failed",
		"failureMessage":"SELECT * FROM users WHERE password='hunter2' AND tenant='acme' token=secret",
		"output":{
			"failureCode":"user_code",
			"ownerUserId":"usr_00000000output_owner",
			"clientMessageId":"usr_00000000output_client"
		},
		"nested":{"authorMemberId":"ubqa7zcm8hy5"}
	}`)
	effect := store.Effect{
		ID: "wfx_baa86a9e8b16589a778ee2cfce0bef62", RunID: "wri_1", ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindMessage,
		Status: store.EffectStatusFailed, ResultJSON: resultJSON,
		CreatedAt: now, UpdatedAt: now,
	}
	response := EffectResponse(effect)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	// Historical failureMessage text never survives; the bounded public
	// problem replaces it. agent.failed is not a stable mapped code, so the
	// unknown prose fails closed to the generic bounded problem.
	for _, secret := range []string{"hunter2", "password", "tenant=acme", "SELECT", "token=secret"} {
		require.NotContains(t, text, secret)
	}
	require.Contains(t, text, "Workflow Run failed")
	// User output survives exactly even with collision keys.
	require.Contains(t, text, "user_code")
	require.Contains(t, text, "usr_00000000output_owner")
	require.Contains(t, text, "usr_00000000output_client")
	// Non-user nested operational keys are redacted.
	require.NotContains(t, text, "ubqa7zcm8hy5")
	// Raw failureMessage key is never projected.
	require.NotContains(t, text, "failureMessage")
}

// TestSchemaKnownFailureProjectionReachesHTTPWire proves the schema-known State
// and EffectResult failure projection reaches the real Run show/effect HTTP
// handlers (the same responses the Agent API, Desktop apiclient, and Exec
// consume): historical nested message/failureMessage text with secret payloads
// never reaches the wire, while user collision values survive exactly.
func TestSchemaKnownFailureProjectionReachesHTTPWire(t *testing.T) {
	gin.SetMode(gin.TestMode)
	now := time.Now().UTC()
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{"code":"message.delivery_failed","message":"SELECT * FROM users WHERE password='hunter2'","stage":"dispatch"},
		"variables":{"ownerUserId":"usr_0000000000user_variable","failure":"user value"},
		"input":{}
	}`)
	fake := &fakeService{run: store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.delivery_failed",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}}
	handler := NewHandler(fake)
	router := gin.New()
	router.GET("/workflow/runs/:runId",
		func(ctx *gin.Context) {
			api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_00000000000001"}})
			api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1", MemberID: "000000000001", MemberUserID: "usr_00000000000001", WorkspaceRole: "owner"})
		},
		handleWorkspace(handler.Get),
	)
	request := httptest.NewRequest(http.MethodGet, "/workflow/runs/wri_1", http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
	body := recorder.Body.String()
	for _, secret := range []string{"hunter2", "password", "SELECT"} {
		require.NotContains(t, body, secret)
	}
	// The bounded problem/hint replace the historical message.
	require.Contains(t, body, "The message could not be delivered")
	// User collision values survive exactly through the wire.
	require.Contains(t, body, "usr_0000000000user_variable")
	require.Contains(t, body, "user value")
	require.NotContains(t, body, `"message":"SELECT`)
}

// TestRunLogDetailsPreservesSharedTruncationDeterminism verifies the Run log
// Details projection carries the exact diagnostic budget truncation metadata
// (omittedCount/truncatedFields/serializedBytes) alongside the bounded
// diagnostics for every failure surface the runtime emits (reducer run_failed,
// effect failure/retry/timeout events, callback completion events), so the
// store→Agent/Desktop log surface reports the identical deterministic cuts as
// the persisted error_json.
func TestRunLogDetailsPreservesSharedTruncationDeterminism(t *testing.T) {
	now := time.Now().UTC()
	payload := json.RawMessage(`{
		"code":"message.target_invalid","stage":"dispatch","path":"notify",
		"problem":"The message target is invalid","hint":"The message target is invalid",
		"diagnostics":[
			{"code":"message.target_invalid","field":"target","message":"safe 0"},
			{"code":"message.target_invalid","field":"target","message":"safe 1"}
		],
		"truncation":{
			"diagnosticsTruncated":true,
			"omittedCount":14,
			"truncatedFields":["message"],
			"serializedBytes":4096
		}
	}`)
	event := store.Event{ID: "wre_t", RunID: "wri_1", Seq: 1, Type: "effect_failed", PayloadJSON: payload, CreatedAt: now}
	log := RunLogResponse(event)
	require.Equal(t, "error", log.Level)
	require.Contains(t, string(log.Details), `"omittedCount":14`)
	require.Contains(t, string(log.Details), `"truncatedFields":["message"]`)
	require.Contains(t, string(log.Details), `"serializedBytes":4096`)
	require.Contains(t, string(log.Details), `"diagnosticsTruncated":true`)
	require.Contains(t, string(log.Details), `"diagnostics":`)

	// The same payload must project identically through Run show (error_json),
	// the event surface, and the log surface: no re-bounding, no recomputation.
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid", ErrorJSON: payload,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	runResponse := RunDetailResponse(run)
	require.JSONEq(t, string(payload), string(runResponse.Error))
	eventResponse := EventResponse(event)
	require.JSONEq(t, string(payload), string(eventResponse.Payload))
}

// TestAdversarialLegacyFailureFieldsFailClosedTyped proves the typed,
// schema-known read-time projection fails closed on every formerly-permissively
// retained field: secrets, malformed types, and oversized values in stage/path/
// retryable/diagnostics/workItem never survive, while the reviewed code map
// derives the safe problem/hint and user collision values stay exact.
func TestAdversarialLegacyFailureFieldsFailClosedTyped(t *testing.T) {
	now := time.Now().UTC()
	// An adversarial legacy Failure row with secret payloads, malformed types,
	// and oversized values in every formerly-retained field.
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{
			"code":"message.target_invalid",
			"message":"SELECT * FROM users WHERE password='hunter2' tenant=acme",
			"stage":"password=hunter2 stage with secret",
			"path":"SELECT secret FROM paths token=abc",
			"retryable":"not-a-bool",
			"problem":"SELECT problem text password=hunter2",
			"hint":999,
			"diagnostics":"not-an-array",
			"workItem":"not-an-object",
			"leaseToken":"operational_secret",
			"ownerUserId":"usr_000operational"
		},
		"input":{"failure":"user input value","ownerUserId":"usr_000000000input_collision"},
		"variables":{"clientMessageId":"usr_00000000000var_collision"}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)

	// Secrets never survive in any formerly-retained field.
	for _, secret := range []string{"hunter2", "password", "acme", "SELECT secret", "SELECT problem", "token=abc", "operational_secret", "usr_000operational"} {
		require.NotContains(t, text, secret)
	}
	// The malformed retryable (string) fails closed: it is not projected.
	require.NotContains(t, text, `"retryable":"not-a-bool"`)
	require.NotContains(t, text, "not-a-bool")
	// The malformed diagnostics (string) fails closed: not projected as an array.
	require.NotContains(t, text, "not-an-array")
	// The malformed workItem (string) fails closed: not projected as an object.
	require.NotContains(t, text, "not-an-object")
	// Operational keys are stripped from the typed failure container.
	require.NotContains(t, text, "leaseToken")
	// The reviewed code map derives the safe problem/hint (the explicit malformed
	// problem text is dropped in favor of the bounded public mapping).
	require.Contains(t, text, "The message target is invalid")
	require.NotContains(t, text, `"message":"SELECT`)
	// User collision values survive exactly at their explicit boundary.
	require.Contains(t, text, "user input value")
	require.Contains(t, text, "usr_000000000input_collision")
	require.Contains(t, text, "usr_00000000000var_collision")
}

// TestAdversarialLegacyOversizedFailureFieldsBounded proves oversized values in
// stage/path/diagnostics/workItem and an oversized serialized failure object are
// bounded/fail closed rather than projected verbatim. Identity fields (code,
// stage, path, diagnostic path, WorkItem identity) fail closed when oversized
// instead of being truncated into a different valid identity; free-text fields
// (message) are bounded.
func TestAdversarialLegacyOversizedFailureFieldsBounded(t *testing.T) {
	now := time.Now().UTC()
	oversized := strings.Repeat("x", 4096)
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{
			"code":"message.target_invalid",
			"stage":"reducer",
			"path":"wnd_` + oversized + `",
			"diagnostics":[
				{"code":"message.target_invalid","path":"wnd_` + oversized + `","message":"` + oversized + `"},
				{"code":"message.target_invalid","path":"wnd_short"}
			]
		}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	// The oversized identity fields (path, diagnostic path) are never projected
	// at full size and never truncated into a different valid identity: they fail
	// closed (dropped). The oversized free-text diagnostic message is bounded.
	require.NotContains(t, text, strings.Repeat("x", 2000))
	require.NotContains(t, text, strings.Repeat("x", 4096))
	// The oversized diagnostic path fails closed (dropped) so only the short one
	// survives.
	require.Contains(t, text, `"path":"wnd_short"`)
	// The reviewed stage survives (it is within the closed vocabulary).
	require.Contains(t, text, `"stage":"reducer"`)
	// The oversized top-level path fails closed: it is NOT projected truncated
	// into a different valid path.
	require.NotContains(t, text, `"path":"wnd_`+strings.Repeat("x", 1024))
	// The oversized diagnostic message is bounded (free text truncates).
	require.Contains(t, text, `"message":"`+strings.Repeat("x", 1024))
}

// TestAdversarialCrossTenantFailureNonEnumeration proves a cross-tenant Run
// selector (forbidden) projects byte-identically to a missing Run selector
// through the public Run API error boundary (classifyError), so a caller cannot
// enumerate Runs it cannot access.
func TestAdversarialCrossTenantFailureNonEnumeration(t *testing.T) {
	missing, ok := classifyError(runmanage.RequestError{
		Kind: storeerr.ErrNotFound, Code: runmanage.ErrorCodeNotFound, Message: "Workflow Run wri_missing is missing",
	})
	require.True(t, ok)
	forbidden, ok := classifyError(runmanage.RequestError{
		Kind: storeerr.ErrForbidden, Code: runmanage.ErrorCodeForbidden, Message: "Workflow Run wri_secret is forbidden in another tenant",
	})
	require.True(t, ok)

	// Byte-identical external contract for missing and cross-tenant/forbidden.
	require.Equal(t, http.StatusNotFound, missing.Status)
	require.Equal(t, http.StatusNotFound, forbidden.Status)
	require.Equal(t, missing.Code, forbidden.Code)
	require.Equal(t, "run_not_found", missing.Code)
	require.Equal(t, missing.Message, forbidden.Message)
	require.Nil(t, missing.Details)
	require.Nil(t, forbidden.Details)
	require.Equal(t, missing.Stage, forbidden.Stage)
	require.Equal(t, missing.Path, forbidden.Path)
	require.Equal(t, missing.Hint, forbidden.Hint)
	require.Equal(t, missing.Retryable, forbidden.Retryable)
	require.Equal(t, missing.Diagnostics, forbidden.Diagnostics)
	for _, secret := range []string{"wri_secret", "forbidden", "missing", "another tenant"} {
		require.NotContains(t, missing.Message, secret)
		require.NotContains(t, forbidden.Message, secret)
	}
}

// TestUnifiedProjectorRejectsSecretBearingWellTypedFields proves the unified
// typed allowlisted projector rejects secret-bearing prose in well-typed
// failure fields (stage/path/problem/hint/message) and derives problem/hint
// only from the reviewed code map, across ErrorJSON, State, and EffectResult.
