package run

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

	"github.com/stretchr/testify/require"

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

// TestEventEnvelopeScopeValidationOnMessageSent proves the closed message-scope
// grammar on a kind that legitimately carries scopes: a well-typed scope pair
// survives, a malformed scope object (missing scopeId or an unknown scope kind)
// fails closed, and an unknown scope kind is dropped.
func TestEventEnvelopeScopeValidationOnMessageSent(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_1", RunID: "wri_1", Seq: 1, Type: "message_sent",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_send","kind":"send_message",
			"messageId":"msg_1",
			"deliveryScope":{"kind":"channel","scopeId":"chn_1"},
			"threadScope":{"kind":"thread","scopeId":"thr_1"},
			"unknownKey":"should_be_dropped",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	// Well-typed scope pairs survive.
	require.Contains(t, text, `"deliveryScope"`)
	require.Contains(t, text, `"kind":"channel"`)
	require.Contains(t, text, `"scopeId":"chn_1"`)
	require.Contains(t, text, `"threadScope"`)
	require.Contains(t, text, `"kind":"thread"`)
	require.Contains(t, text, `"scopeId":"thr_1"`)
	// Unknown keys and operational keys are dropped.
	require.NotContains(t, text, "should_be_dropped")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeProjectionRejectsMalformedTypedFields proves the closed typed
// schema rejects well-typed secrets, wrong prefixes, wrong kinds, malformed
// times, and malformed list members on a failure-carrying event envelope, while
// preserving the reviewed context. message_callback_awaiting carries the agent
// wake list; effect_retry_scheduled carries the retryAt/receiptRecovery fields.
func TestEventEnvelopeProjectionRejectsMalformedTypedFields(t *testing.T) {
	now := time.Now().UTC()

	// effect_retry_scheduled: malformed retryAt and non-bool receiptRecovery.
	retryEvent := store.Event{
		ID: "wre_1", RunID: "wri_1", Seq: 1, Type: "effect_retry_scheduled",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_dispatch","kind":"send_message",
			"code":"message.delivery_failed","stage":"dispatch","path":"wnd_dispatch",
			"retryAt":"not-a-time","receiptRecovery":"not-a-bool","messageId":"wrong_prefix",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	retryLog := RunLogResponse(retryEvent)
	retryText := string(retryLog.Details)
	require.Contains(t, retryText, `"code":"message.delivery_failed"`)
	require.Contains(t, retryText, "The message could not be delivered")
	require.NotContains(t, retryText, "not-a-time")
	require.NotContains(t, retryText, "not-a-bool")
	require.NotContains(t, retryText, "wrong_prefix")
	require.NotContains(t, retryText, "leaseToken")
	require.NotContains(t, retryText, "lease_secret")

	// message_callback_awaiting: malformed agent list member is dropped, the
	// valid member survives.
	callbackEvent := store.Event{
		ID: "wre_2", RunID: "wri_1", Seq: 2, Type: "message_callback_awaiting",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_cb","kind":"send_message",
			"messageId":"msg_1","callbackResponderAgentId":"agt_0000000000000a",
			"deliveryScope":{"kind":"task","scopeId":"tsk_1"},
			"wakeAgentIds":["not_an_agent","agt_000000000000ok"],
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	callbackLog := RunLogResponse(callbackEvent)
	callbackText := string(callbackLog.Details)
	require.NotContains(t, callbackText, "not_an_agent")
	require.Contains(t, callbackText, "agt_000000000000ok")
	require.NotContains(t, callbackText, "leaseToken")
	require.NotContains(t, callbackText, "lease_secret")
}

// TestEventEnvelopeProjectionRejectsWrongKindValue proves the closed kind enum
// rejects an invalid kind value (prose, a secret, or a wrong kind) and
// drops the whole envelope context, preserving only the reviewed failure
// contract.
func TestEventEnvelopeProjectionRejectsWrongKindValue(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":"token=SECRET",
			"code":"message.delivery_failed",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	// The wrong kind is dropped; the failure contract still projects.
	require.NotContains(t, text, "kind")
	require.NotContains(t, text, "token=SECRET")
	require.NotContains(t, text, "SECRET")
	require.Contains(t, text, "The message could not be delivered")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeProjectionDropsUnknownEventKind proves an event kind with no
// reviewed schema fails closed: the scrubbed payload keeps no unknown fields and
// no secret-bearing content survives.
func TestEventEnvelopeProjectionDropsUnknownEventKind(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_1", RunID: "wri_1", Seq: 1, Type: "unknown_event_kind",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"code":"message.delivery_failed",
			"secret":"hunter2","password":"hunter2","tenant":"acme",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	require.NotContains(t, text, "hunter2")
	require.NotContains(t, text, "password")
	require.NotContains(t, text, "acme")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeProjectionTruncationAndOversizedBounded proves oversized
// envelope context fields and an over-limit wakeAgentIds list are bounded, and
// no secret survives.
func TestEventEnvelopeProjectionTruncationAndOversizedBounded(t *testing.T) {
	now := time.Now().UTC()
	oversized := strings.Repeat("x", 4096)
	event := store.Event{
		ID: "wre_1", RunID: "wri_1", Seq: 1, Type: "message_callback_awaiting",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_` + oversized + `","activationId":"a00000000000000000001","nodeId":"wnd_cb","kind":"send_message",
			"messageId":"msg_1","callbackResponderAgentId":"agt_0000000000000a",
			"wakeAgentIds":["agt_0000000000000a","agt_0000000000000b","agt_0000000000000c","agt_0000000000000d","agt_0000000000000e","agt_0000000000000f","agt_0000000000000g","agt_0000000000000h","agt_0000000000000i","agt_0000000000000j"],
			"deliveryScope":{"kind":"task","scopeId":"tsk_1"},
			"secret":"hunter2"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	// Oversized effectId is bounded (dropped or truncated), context survives.
	require.Contains(t, text, `"activationId":"a00000000000000000001"`)
	require.Contains(t, text, `"kind":"send_message"`)
	require.NotContains(t, text, strings.Repeat("x", 2000))
	require.NotContains(t, text, "hunter2")
}

// TestEffectResultNonFailureFieldsClosedTypedSchema proves the flat EffectResult
// non-failure fields are validated against the shared closed typed schema:
// valid enums/IDs/scopes survive, malformed/secret-bearing/unknown values are
// dropped, and the user-authored output is preserved exactly.
func TestEffectResultNonFailureFieldsClosedTypedSchema(t *testing.T) {
	now := time.Now().UTC()
	resultJSON := json.RawMessage(`{
		"port":"failed",
		"kind":"send_message",
		"status":"awaiting",
		"messageId":"msg_1",
		"notificationId":"btn_baa86a9e8b16589a778ee2cfce0bef62",
		"assigneeMemberId":"000000000001",
		"decidedByMemberId":"000000000002",
		"callbackResponderAgentId":"agt_0000000000000a",
		"retryAt":"2026-08-16T12:00:00Z",
		"receiptRecovery":true,
		"wakeAgentIds":["agt_0000000000000a","agt_0000000000000b"],
		"deliveryScope":{"kind":"task","scopeId":"tsk_1"},
		"threadScope":{"kind":"thread","scopeId":"thr_1"},
		"workScope":{"kind":"channel","scopeId":"chn_1"},
		"portBad":"token=SECRET",
		"messageIdBad":"wrong_prefix",
		"retryAtBad":"not-a-time",
		"wakeAgentIdsBad":["not_an_agent"],
		"output":{"failure":"user output","failureCode":"user_code"}
	}`)
	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)
	text := string(raw)
	// Valid non-failure fields survive. `kind`/`status` are the Effect row's
	// store fields (EffectResponse DTO), NOT flat ResultJSON fields; the invented
	// flat ResultJSON `kind`/`status`/`retryAt`/`receiptRecovery` (which are NOT
	// orchestrator.EffectResult fields) must fail closed and never project.
	require.Contains(t, text, `"port":"failed"`)
	require.Contains(t, text, `"kind":"send_message"`)
	require.Contains(t, text, `"status":"failed"`)
	require.Contains(t, text, `"messageId":"msg_1"`)
	require.Contains(t, text, `"notificationId":"btn_baa86a9e8b16589a778ee2cfce0bef62"`)
	require.Contains(t, text, `"assigneeMemberId":"000000000001"`)
	require.Contains(t, text, `"decidedByMemberId":"000000000002"`)
	require.Contains(t, text, `"callbackResponderAgentId":"agt_0000000000000a"`)
	require.Contains(t, text, "agt_0000000000000a")
	require.Contains(t, text, "agt_0000000000000b")
	require.Contains(t, text, `"deliveryScope"`)
	require.Contains(t, text, `"threadScope"`)
	require.Contains(t, text, `"workScope"`)
	// Malformed/unknown/secret-bearing non-failure fields are dropped, INCLUDING
	// the invented flat ResultJSON kind/status/retryAt/receiptRecovery fields.
	require.NotContains(t, text, `"status":"awaiting"`)
	require.NotContains(t, text, `"retryAt":"2026-08-16T12:00:00Z"`)
	require.NotContains(t, text, `"receiptRecovery":true`)
	require.NotContains(t, text, "portBad")
	require.NotContains(t, text, "messageIdBad")
	require.NotContains(t, text, "wrong_prefix")
	require.NotContains(t, text, "retryAtBad")
	require.NotContains(t, text, "not-a-time")
	require.NotContains(t, text, "wakeAgentIdsBad")
	require.NotContains(t, text, "not_an_agent")
	require.NotContains(t, text, "token=SECRET")
	require.NotContains(t, text, "SECRET")
	// User output is preserved exactly.
	require.Contains(t, text, "user output")
	require.Contains(t, text, "user_code")
}

// TestEventEnvelopeFlatFailureDoesNotInheritEffectResultOutputExemption proves a
// flat {failureCode, ...} EffectResult inside an event envelope must NOT inherit
// the EffectResult response's open `output` exemption: the event-envelope path
// never carries an arbitrary `output` field, so a secret embedded in output can
// never project through an event.
func TestEventEnvelopeFlatFailureDoesNotInheritEffectResultOutputExemption(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_o", RunID: "wri_1", Seq: 1, Type: "effect_failed",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1",
			"failureCode":"message.delivery_failed",
			"failureMessage":"raw cause",
			"output":{"secret":"output.secret=hunter2","failure":"user output"},
			"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"`)
	// The event-envelope path never inherits the open output exemption.
	require.NotContains(t, text, "output.secret=hunter2")
	require.NotContains(t, text, "user output")
	require.NotContains(t, text, `"output"`)
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeDropsUnknownFailurePrefixedSecrets proves unknown `failure*`
// fields (failureSecret, failureToken, corruption-controlled values) are dropped
// by the exact failure-namespace allowlist — they must NOT survive merely because
// they carry the `failure` prefix.
func TestEventEnvelopeDropsUnknownFailurePrefixedSecrets(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_s", RunID: "wri_1", Seq: 1, Type: "effect_failed",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1",
			"failureCode":"message.delivery_failed",
			"failureSecret":"s3cr3t-token",
			"failureToken":"tok_abc123",
			"failureMessageRaw":"SELECT secret FROM vault",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	require.Contains(t, text, `"failureCode":"message.delivery_failed"`)
	require.NotContains(t, text, "s3cr3t-token")
	require.NotContains(t, text, "tok_abc123")
	require.NotContains(t, text, "SELECT secret FROM vault")
	require.NotContains(t, text, "failureSecret")
	require.NotContains(t, text, "failureToken")
	require.NotContains(t, text, "failureMessageRaw")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeRejectsFailureOnNonFailureKind proves a failure contract on an
// event kind that never carries one fails closed: no context field survives.
func TestEventEnvelopeRejectsFailureOnNonFailureKind(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_f", RunID: "wri_1", Seq: 1, Type: "message_sent",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"messageId":"msg_1",
			"code":"message.delivery_failed","stage":"dispatch","path":"wnd_1",
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	require.NotContains(t, text, "message.delivery_failed")
	require.NotContains(t, text, `"effectId"`)
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopeRejectsMalformedActorIDs proves run_canceled actor provenance
// validates actor user/agent/member IDs with authoritative grammars so a
// token-like corruption cannot project.
func TestEventEnvelopeRejectsMalformedActorIDs(t *testing.T) {
	now := time.Now().UTC()
	event := store.Event{
		ID: "wre_a", RunID: "wri_1", Seq: 1, Type: "run_canceled",
		PayloadJSON: json.RawMessage(`{
			"kind":"run_canceled",
			"reason":"canceled",
			"actor":{"type":"human","id":"token=SECRET","memberId":"0000000actor"},
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	require.NotContains(t, text, "token=SECRET")
	require.NotContains(t, text, "SECRET")
	require.NotContains(t, text, "leaseToken")
	require.NotContains(t, text, "lease_secret")
}

// TestEventEnvelopePreservesReal32HexMessageID proves a real app-facing message
// id (identifiers.NewMessageID: raw 32 lowercase hex, no prefix) survives the
// envelope projection, while a prose/secret message id fails closed.
func TestEventEnvelopePreservesReal32HexMessageID(t *testing.T) {
	now := time.Now().UTC()
	realID := "0123456789abcdef0123456789abcdef"
	event := store.Event{
		ID: "wre_h", RunID: "wri_1", Seq: 1, Type: "message_sent",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"messageId":"` + realID + `",
			"deliveryScope":{"kind":"channel","scopeId":"chn_1"}
		}`),
		CreatedAt: now,
	}
	log := RunLogResponse(event)
	text := string(log.Details)
	require.Contains(t, text, `"messageId":"`+realID+`"`, "a real 32-hex message id must survive")

	badEvent := store.Event{
		ID: "wre_h2", RunID: "wri_1", Seq: 2, Type: "message_sent",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"messageId":"select * from users where password=hunter2"
		}`),
		CreatedAt: now,
	}
	badText := string(RunLogResponse(badEvent).Details)
	require.NotContains(t, badText, "hunter2", "a prose/secret message id must fail closed")
	require.NotContains(t, badText, `"messageId"`, "an invalid message id must not survive")
}
