package run

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

	"github.com/stretchr/testify/require"

	store "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
)

// TestEventEnvelopeProjectionPreservesReviewedContextAcrossEventKinds proves
// the schema-known per-event envelope projection preserves the reviewed bounded
// activation/node/scope/effect/kind/retry/message/receipt/recovery context for
// every runtime event category (reducer, fan-out/WorkItem, callback,
// retry-scheduled, timeout, approval, delivery, recovery) while projecting ONLY
// the failure contract through the shared redaction/budget logic, and never
// leaks operational keys nested in the payload.
func TestEventEnvelopeProjectionPreservesReviewedContextAcrossEventKinds(t *testing.T) {
	now := time.Now().UTC()
	oversized := strings.Repeat("x", 4096)

	tests := []struct {
		name        string
		kind        string
		payload     string
		wantPresent []string
		wantAbsent  []string
	}{
		{
			name: "reducer failure keeps activation node kind + failure contract",
			kind: "run_failed",
			payload: `{
				"kind":"run_failed","activationId":"a00000000000000000001","nodeId":"wnd_1",
				"code":"run.graph_deadlocked","stage":"reducer","path":"wnd_1",
				"problem":"The workflow graph is deadlocked","hint":"The workflow graph is deadlocked",
				"retryable":true,"workItem":{"key":"child","index":1},
				"leaseToken":"lease_secret","ownerUserId":"usr_00000000secret"
			}`,
			wantPresent: []string{
				`"kind":"run_failed"`, `"activationId":"a00000000000000000001"`, `"nodeId":"wnd_1"`,
				`"code":"run.graph_deadlocked"`, `"stage":"reducer"`, `"path":"wnd_1"`,
				"The workflow graph is deadlocked", `"retryable":true`, `"key":"child"`,
			},
			wantAbsent: []string{"leaseToken", "lease_secret", "usr_00000000secret", "ownerUserId"},
		},
		{
			name: "fan-out work item failure keeps scope workItem + failure contract",
			kind: "fan_out_item_failed",
			payload: `{
				"kind":"fan_out_item_failed","scopeId":"wsc_1",
				"code":"work_item.dependency_blocked","stage":"work_item","path":"wnd_fan",
				"workItem":{"key":"child","index":2,"dependencyPath":"dep"},
				"idempotencyKey":"idem_secret"
			}`,
			wantPresent: []string{
				`"kind":"fan_out_item_failed"`, `"scopeId":"wsc_1"`,
				`"code":"work_item.dependency_blocked"`, `"stage":"work_item"`, `"path":"wnd_fan"`,
				`"key":"child"`, `"dependencyPath":"dep"`,
			},
			wantAbsent: []string{"idempotencyKey", "idem_secret"},
		},
		{
			name: "callback awaiting keeps effect message receipt delivery context",
			kind: "message_callback_awaiting",
			payload: `{
				"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_cb","kind":"send_message",
				"messageId":"msg_1","wakeAgentIds":["agt_0000000000000a","agt_0000000000000b"],
				"callbackResponderAgentId":"agt_0000000000000a",
				"deliveryScope":{"kind":"task","scopeId":"tsk_1"},
				"clientMessageId":"client_secret"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`, `"activationId":"a00000000000000000001"`, `"nodeId":"wnd_cb"`, `"kind":"send_message"`,
				`"messageId":"msg_1"`, `"agt_0000000000000a"`, `"agt_0000000000000b"`, `"callbackResponderAgentId":"agt_0000000000000a"`,
				`"deliveryScope"`, `"scopeId":"tsk_1"`,
			},
			wantAbsent: []string{"clientMessageId", "client_secret"},
		},
		{
			name: "callback completed failure keeps agent port + failure contract",
			kind: "message_callback_completed",
			payload: `{
				"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_cb","agentId":"agt_0000000000000a","port":"failed",
				"code":"callback.responder_changed","stage":"callback","path":"wnd_cb",
				"problem":"The callback responder changed after delivery","hint":"The callback responder changed after delivery",
				"leaseToken":"lease_secret"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`, `"activationId":"a00000000000000000001"`, `"nodeId":"wnd_cb"`, `"agentId":"agt_0000000000000a"`, `"port":"failed"`,
				`"code":"callback.responder_changed"`, `"stage":"callback"`, `"path":"wnd_cb"`,
				"The callback responder changed after delivery",
			},
			wantAbsent: []string{"leaseToken", "lease_secret"},
		},
		{
			name: "retry-scheduled keeps retry receipt recovery context + failure contract",
			kind: "effect_retry_scheduled",
			payload: `{
				"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_dispatch","kind":"send_message",
				"code":"message.delivery_failed","stage":"dispatch","path":"wnd_dispatch",
				"problem":"The message could not be delivered","hint":"The message could not be delivered",
				"retryable":true,"retryAt":"2026-08-16T12:00:00Z",
				"messageId":"msg_1","receiptRecovery":true,
				"leaseToken":"lease_secret"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`, `"activationId":"a00000000000000000001"`, `"nodeId":"wnd_dispatch"`, `"kind":"send_message"`,
				`"code":"message.delivery_failed"`, `"stage":"dispatch"`, `"path":"wnd_dispatch"`,
				"The message could not be delivered", `"retryable":true`,
				`"retryAt":"2026-08-16T12:00:00Z"`, `"messageId":"msg_1"`, `"receiptRecovery":true`,
			},
			wantAbsent: []string{"leaseToken", "lease_secret"},
		},
		{
			name: "timeout keeps effect node kind + failure contract",
			kind: "effect_timed_out",
			payload: `{
				"effectId":"wfx_038fc924dd87bec4ad039322818f92cc","activationId":"a00000000000000000002","nodeId":"wait","kind":"request_approval",
				"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"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_038fc924dd87bec4ad039322818f92cc"`, `"activationId":"a00000000000000000002"`, `"nodeId":"wait"`, `"kind":"request_approval"`,
				`"code":"effect.timed_out"`, `"stage":"dispatch"`, `"path":"wait"`,
				"The workflow effect timed out", `"retryable":true`,
			},
			wantAbsent: []string{"leaseToken", "lease_secret"},
		},
		{
			name: "approval awaiting keeps notification assignee context",
			kind: "approval_awaiting",
			payload: `{
				"effectId":"wfx_da6422670e96e26689cb18e694d283ae","activationId":"a00000000000000000003","nodeId":"wnd_approve","kind":"request_approval",
				"notificationId":"btn_baa86a9e8b16589a778ee2cfce0bef62","assigneeMemberId":"000000000001",
				"leaseToken":"lease_secret"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_da6422670e96e26689cb18e694d283ae"`, `"activationId":"a00000000000000000003"`, `"nodeId":"wnd_approve"`, `"kind":"request_approval"`,
				`"notificationId":"btn_baa86a9e8b16589a778ee2cfce0bef62"`, `"assigneeMemberId":"000000000001"`,
			},
			wantAbsent: []string{"leaseToken", "lease_secret"},
		},
		{
			name: "approval decided keeps decision decided-by context",
			kind: "approval_decided",
			payload: `{
				"effectId":"wfx_da6422670e96e26689cb18e694d283ae","activationId":"a00000000000000000003","nodeId":"wnd_approve",
				"notificationId":"btn_baa86a9e8b16589a778ee2cfce0bef62","decision":"approved","decidedByMemberId":"000000000002",
				"ownerMemberId":"000000secret"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_da6422670e96e26689cb18e694d283ae"`, `"activationId":"a00000000000000000003"`, `"nodeId":"wnd_approve"`,
				`"notificationId":"btn_baa86a9e8b16589a778ee2cfce0bef62"`, `"decision":"approved"`, `"decidedByMemberId":"000000000002"`,
			},
			wantAbsent: []string{"ownerMemberId", "000000secret"},
		},
		{
			name: "delivery keeps message scope thread context",
			kind: "message_sent",
			payload: `{
				"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_send","kind":"send_message",
				"messageId":"msg_1",
				"deliveryScope":{"kind":"channel","scopeId":"ch_1"},
				"threadScope":{"kind":"thread","scopeId":"th_1"},
				"idempotencyKey":"idem_secret"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`, `"activationId":"a00000000000000000001"`, `"nodeId":"wnd_send"`, `"kind":"send_message"`,
				`"messageId":"msg_1"`, `"deliveryScope"`, `"threadScope"`, `"scopeId":"ch_1"`, `"scopeId":"th_1"`,
			},
			wantAbsent: []string{"idempotencyKey", "idem_secret"},
		},
		{
			name: "recovery keeps receiptRecovery marker + failure contract",
			kind: "effect_retry_scheduled",
			payload: `{
				"effectId":"wfx_2ad429f1209151b9b44d940dc497045a","activationId":"a00000000000000000004","nodeId":"wnd_rec","kind":"send_message",
				"code":"message.writer_unavailable","stage":"dispatch","path":"wnd_rec",
				"problem":"The message writer is unavailable","hint":"The message writer is temporarily unavailable",
				"retryable":true,"receiptRecovery":true,
				"messageId":"msg_r","retryAt":"2026-08-16T13:00:00Z"
			}`,
			wantPresent: []string{
				`"effectId":"wfx_2ad429f1209151b9b44d940dc497045a"`, `"activationId":"a00000000000000000004"`, `"nodeId":"wnd_rec"`, `"kind":"send_message"`,
				`"code":"message.writer_unavailable"`, `"receiptRecovery":true`, `"messageId":"msg_r"`,
				"The message writer is unavailable",
			},
			wantAbsent: []string{},
		},
		{
			name: "oversized envelope fields are bounded",
			kind: "effect_failed",
			payload: `{
				"effectId":"wfx_` + oversized + `","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
				"code":"message.delivery_failed","stage":"dispatch","path":"wnd_1",
				"problem":"The message could not be delivered","hint":"The message could not be delivered",
				"wakeAgentIds":["` + oversized + `"]
			}`,
			wantPresent: []string{`"activationId":"a00000000000000000001"`, `"code":"message.delivery_failed"`},
			wantAbsent:  []string{strings.Repeat("x", 2000)},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			event := store.Event{
				ID: "wre_1", RunID: "wri_1", Seq: 1, Type: test.kind,
				PayloadJSON: json.RawMessage(test.payload), CreatedAt: now,
			}
			// Run log surface.
			log := RunLogResponse(event)
			logText := string(log.Details)
			for _, want := range test.wantPresent {
				require.Contains(t, logText, want, "log Details must preserve %q", want)
			}
			for _, absent := range test.wantAbsent {
				require.NotContains(t, logText, absent, "log Details must not contain %q", absent)
			}
			// Event surface.
			eventResp := EventResponse(event)
			eventText := string(eventResp.Payload)
			for _, want := range test.wantPresent {
				require.Contains(t, eventText, want, "event Payload must preserve %q", want)
			}
			for _, absent := range test.wantAbsent {
				require.NotContains(t, eventText, absent, "event Payload must not contain %q", absent)
			}
			require.JSONEq(t, logText, eventText, "log Details and event Payload must project identically")
		})
	}
}

// TestEventEnvelopeProjectionAdversarialMalformedFields proves the typed
// envelope allowlist fails closed on malformed, secret-bearing, or unknown
// envelope fields: a secret carried in a well-typed envelope field is bounded,
// a malformed scope object or boolean is dropped, an unknown key is dropped, and
// a legacy {code, message} failure inside an event envelope is projected safely.
func TestEventEnvelopeProjectionAdversarialMalformedFields(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_1", RunID: "wri_1", Seq: 1, Type: "effect_failed",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"code":"message.delivery_failed",
			"message":"SELECT * FROM users WHERE password='hunter2' tenant=acme",
			"receiptRecovery":"not-a-bool",
			"deliveryScope":{"kind":"task","scopeId":"tsk_1","extra":"leak"},
			"threadScope":{"kind":"task"},
			"unknownKey":"should_be_dropped",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	// The reviewed failure contract derives the bounded problem; the arbitrary
	// legacy message is never projected.
	require.Contains(t, text, "The message could not be delivered")
	require.NotContains(t, text, "hunter2")
	require.NotContains(t, text, "password")
	require.NotContains(t, text, "tenant=acme")
	require.NotContains(t, text, "SELECT")
	// Envelope context survives.
	require.Contains(t, text, `"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`)
	require.Contains(t, text, `"activationId":"a00000000000000000001"`)
	require.Contains(t, text, `"kind":"send_message"`)
	// Malformed typed fields fail closed.
	require.NotContains(t, text, "not-a-bool")
	require.NotContains(t, text, "receiptRecovery")
	// effect_failed does not carry message-scope context: the deliveryScope/
	// threadScope fields are unknown for this kind and are dropped entirely.
	require.NotContains(t, text, "deliveryScope")
	require.NotContains(t, text, "threadScope")
	require.NotContains(t, text, `"scopeId":"tsk_1"`)
	require.NotContains(t, text, `"extra":"leak"`)
	require.NotContains(t, text, "should_be_dropped")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeProjectionFlatEffectResultDropsRawMessage proves a flat
// {failureCode, ...} EffectResult embedded in an event envelope always drops the
// raw failureMessage while keeping the reviewed failureCode and any safe
// envelope context.
func TestEventEnvelopeProjectionFlatEffectResultDropsRawMessage(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_1", RunID: "wri_1", Seq: 1, Type: "effect_failed",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1",
			"failureCode":"message.delivery_failed",
			"failureMessage":"SELECT * FROM users WHERE password='hunter2' tenant=acme",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	require.Contains(t, text, `"failureCode":"message.delivery_failed"`)
	require.Contains(t, text, `"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`)
	require.NotContains(t, text, "hunter2")
	require.NotContains(t, text, "password")
	require.NotContains(t, text, "tenant=acme")
	require.NotContains(t, text, "failureMessage")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}
