package orchestrator

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

func TestMessageRequestValidateRequiresFrozenIdentityAndEffectIdentity(t *testing.T) {
	request := MessageRequest{
		WorkspaceID: "wsp_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		RunID: "wri_1", ActivationID: "wra_1", EffectID: "wrf_1",
		NodeID: "ask", SourceKind: "manual", OwnerUserID: "usr_1",
		AuthorMemberID: "mem_1", TargetRef: "#ops", Content: "@agent answer",
		ClientMessageID: "workflow-run:wri_1:wrf_1",
	}
	require.NoError(t, request.Validate())

	request.AuthorMemberID = ""
	require.EqualError(t, request.Validate(), "workflow Run message authorMemberId is required")
}

func TestMessageRequestValidateRequiresResolvedCallbackResponder(t *testing.T) {
	t.Parallel()

	request := MessageRequest{
		WorkspaceID: "wsp_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		RunID: "wri_1", ActivationID: "wra_1", EffectID: "wrf_1",
		NodeID: "ask", SourceKind: "manual", OwnerUserID: "usr_1",
		AuthorMemberID: "mem_1", TargetRef: "#ops", Content: "answer",
		ClientMessageID:   "workflow-run:wri_1:wrf_1",
		Callback:          true,
		ResponderAgentRef: " agt_exact ",
	}
	require.EqualError(t, request.Validate(), "workflow Run message responderAgentRef must be canonical")

	request.ResponderAgentRef = "agt_exact"
	require.NoError(t, request.Validate())

	request.Callback = false
	require.EqualError(t, request.Validate(), "non-callback workflow Run message cannot carry responderAgentRef")
}

// selectOneValidationRequest returns a valid callback message carrying a
// selectOne candidate pool, so strategy-specific assertions start from a
// normalized baseline.
func selectOneValidationRequest() MessageRequest {
	return MessageRequest{
		WorkspaceID: "wsp_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		RunID: "wri_1", ActivationID: "wra_1", EffectID: "wrf_1",
		NodeID: "ask", SourceKind: "manual", OwnerUserID: "usr_1",
		AuthorMemberID: "mem_1", TargetRef: "#ops", Content: "answer",
		ClientMessageID:     "workflow-run:wri_1:wrf_1",
		Callback:            true,
		ResponderCandidates: []string{"agt_a", "agt_b"},
		SelectOneStrategy:   "",
	}
}

// TestMessageRequestValidateRejectsUnknownSelectOneStrategy proves the persisted
// MessageRequest validation rejects an unknown strategy value (never normalizing
// it to ordered) BEFORE any dispatch/write.
func TestMessageRequestValidateRejectsUnknownSelectOneStrategy(t *testing.T) {
	t.Parallel()
	request := selectOneValidationRequest()
	request.SelectOneStrategy = plan.SelectOneStrategy("weighted")
	require.ErrorContains(t, request.Validate(), "invalid selectOne strategy")
}

// TestMessageRequestValidateRejectsOrphanSelectOneStrategy proves a strategy
// without responder candidates (orphan) is rejected before dispatch/write.
func TestMessageRequestValidateRejectsOrphanSelectOneStrategy(t *testing.T) {
	t.Parallel()
	request := selectOneValidationRequest()
	request.ResponderCandidates = nil
	request.SelectOneStrategy = plan.SelectOneStrategyRandom
	require.ErrorContains(t, request.Validate(), "only valid with responder candidates")
}

// TestMessageRequestValidateRejectsRandomSingleton proves a random strategy with
// fewer than two deduplicated candidates is rejected before dispatch/write.
func TestMessageRequestValidateRejectsRandomSingleton(t *testing.T) {
	t.Parallel()
	request := selectOneValidationRequest()
	request.ResponderCandidates = []string{"agt_a"}
	request.SelectOneStrategy = plan.SelectOneStrategyRandom
	require.ErrorContains(t, request.Validate(), "at least two deduplicated candidates")
	// Duplicates are rejected even earlier by the normalized pool check (also
	// fail-closed, never reaching a random draw over one distinct candidate).
	request.ResponderCandidates = []string{"agt_a", "agt_a"}
	require.ErrorContains(t, request.Validate(), "duplicated")
}

// TestMessageRequestValidateAcceptsOmittedOrderedAndRandom proves the closed
// strategy boundary admits the three legal forms on the persisted request.
func TestMessageRequestValidateAcceptsOmittedOrderedAndRandom(t *testing.T) {
	t.Parallel()
	for _, strategy := range []plan.SelectOneStrategy{"", plan.SelectOneStrategyOrdered, plan.SelectOneStrategyRandom} {
		request := selectOneValidationRequest()
		request.SelectOneStrategy = strategy
		require.NoError(t, request.Validate(), "strategy %q must validate", strategy)
	}
}

// TestMessageRequestPersistedStrategyPresenceNormalized proves the persisted
// RequestJSON recovery distinguishes an absent selectOneStrategy field (legacy
// ordered default) from an explicit present field, which must resolve to exactly
// "ordered" or "random". An explicit empty/null/whitespace-padded/case-variant or
// unknown value in a tampered RequestJSON is rejected.
func TestMessageRequestPersistedStrategyPresenceNormalized(t *testing.T) {
	t.Parallel()
	base := selectOneValidationRequest()

	// Absent field alone decodes to the legacy ordered default.
	absent, err := json.Marshal(base)
	require.NoError(t, err)
	decoded := MessageRequest{}
	require.NoError(t, json.Unmarshal(absent, &decoded))
	require.False(t, decoded.selectOneStrategyPresent, "a marshaled request omits the omitted strategy field")
	require.Equal(t, plan.SelectOneStrategy(""), decoded.SelectOneStrategy)
	require.NoError(t, decoded.Validate())

	for _, present := range []string{"ordered", "random"} {
		request := base
		request.SelectOneStrategy = plan.SelectOneStrategy(present)
		raw, err := json.Marshal(request)
		require.NoError(t, err)
		decoded = MessageRequest{}
		require.NoError(t, json.Unmarshal(raw, &decoded))
		require.True(t, decoded.selectOneStrategyPresent, "present %s must be recorded", present)
		require.NoError(t, decoded.Validate())
	}

	// Tampered RequestJSON carrying an explicit empty/whitespace/case/unknown
	// strategy must be rejected on recovery — either at decode (non-string wire
	// value) or at Validate (present field not exactly ordered/random).
	for _, wire := range []string{
		`{"selectOneStrategy":""}`,
		`{"selectOneStrategy":" random "}`,
		`{"selectOneStrategy":"Random"}`,
		`{"selectOneStrategy":"weighted"}`,
		`{"selectOneStrategy":null}`,
		`{"selectOneStrategy":7}`,
	} {
		recovered := MessageRequest{}
		if err := json.Unmarshal([]byte(wire), &recovered); err != nil {
			// Decode-time rejection is fail-closed.
			continue
		}
		recovered.WorkspaceID = base.WorkspaceID
		recovered.PlanID = base.PlanID
		recovered.PlanVersionID = base.PlanVersionID
		recovered.RunID = base.RunID
		recovered.ActivationID = base.ActivationID
		recovered.EffectID = base.EffectID
		recovered.NodeID = base.NodeID
		recovered.SourceKind = base.SourceKind
		recovered.OwnerUserID = base.OwnerUserID
		recovered.AuthorMemberID = base.AuthorMemberID
		recovered.TargetRef = base.TargetRef
		recovered.Content = base.Content
		recovered.ClientMessageID = base.ClientMessageID
		recovered.Callback = true
		recovered.ResponderCandidates = base.ResponderCandidates
		require.Error(t, recovered.Validate(), "tampered persisted strategy %s must fail Validate", wire)
	}
}

func TestMessageResultNormalizeDeduplicatesWakeAgents(t *testing.T) {
	deliveryScope := domain.MessageScopeValue{Kind: domain.MessageScopeKindThread, ScopeID: "thr_1"}
	result := (MessageResult{
		MessageID: " msg_1 ", WakeAgentIDs: []string{"agt_b", "", "agt_a", "agt_b"}, DeliveryScope: &deliveryScope,
	}).Normalize()
	require.Equal(t, "msg_1", result.MessageID)
	require.Equal(t, []string{"agt_a", "agt_b"}, result.WakeAgentIDs)
	require.NoError(t, result.Validate())
	require.NotSame(t, &deliveryScope, result.DeliveryScope)
}

func TestMessageResultValidateRequiresNormalizedDeliveryScope(t *testing.T) {
	result := MessageResult{MessageID: "msg_1"}
	require.EqualError(t, result.Validate(), "workflow Run message result deliveryScope is required")

	result.DeliveryScope = &domain.MessageScopeValue{Kind: domain.MessageScopeKind("room"), ScopeID: "scp_1"}
	require.EqualError(t, result.Validate(), "workflow Run message result deliveryScope is invalid: unsupported message scope kind \"room\"")

	result.DeliveryScope = &domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "   "}
	require.EqualError(t, result.Validate(), "workflow Run message result deliveryScope is invalid: message scope id must not have surrounding whitespace")

	result.MessageID = " "
	result.DeliveryScope = &domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_1"}
	require.EqualError(t, result.Validate(), "workflow Run message result messageId is required")
}

func TestMessageResultValidateRequiresNormalizedThreadScope(t *testing.T) {
	t.Parallel()

	result := MessageResult{
		MessageID: "msg_1",
		DeliveryScope: &domain.MessageScopeValue{
			Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn",
		},
		ThreadScope: &domain.MessageScopeValue{
			Kind: domain.MessageScopeKindChannel, ScopeID: "chn_bcdefghijklmno",
		},
	}
	require.EqualError(t, result.Validate(), "workflow Run message result threadScope is invalid")
	result.ThreadScope.Kind = domain.MessageScopeKindThread
	require.NoError(t, result.Validate())
}

// TestEffectResultNormalizeFailsClosedOnIdentityWhitespaceAndUTF8 proves
// EffectResult.Normalize validates failure identity and port fields byte-for-byte
// and fail-closes a spaced/malformed value to empty instead of trimming or
// UTF-8-sanitizing it into a different valid identity. Free-text failure
// message/problem/hint may still trim.
func TestEffectResultNormalizeFailsClosedOnIdentityWhitespaceAndUTF8(t *testing.T) {
	result := EffectResult{
		Port:           " complete ",
		FailureCode:    " child-item ",
		FailureStage:   " reducer ",
		FailurePath:    " $.input.x ",
		FailureMessage: " a real message ",
		FailureProblem: " a problem ",
		FailureHint:    " a hint ",
	}
	normalized := result.Normalize()
	require.Empty(t, normalized.Port, "spaced port must fail closed to empty")
	require.Empty(t, normalized.FailureCode, "spaced failure code must fail closed to empty")
	require.Empty(t, normalized.FailureStage, "spaced failure stage must fail closed to empty")
	require.Empty(t, normalized.FailurePath, "spaced failure path must fail closed to empty")
	require.Equal(t, "a real message", normalized.FailureMessage, "free-text message may trim")
	require.Equal(t, "a problem", normalized.FailureProblem, "free-text problem may trim")
	require.Equal(t, "a hint", normalized.FailureHint, "free-text hint may trim")

	malformed := EffectResult{
		Port:        "complete\xffitem",
		FailureCode: "child\xffitem",
	}
	normalized = malformed.Normalize()
	require.Empty(t, normalized.Port, "malformed-UTF-8 port must fail closed to empty")
	require.Empty(t, normalized.FailureCode, "malformed-UTF-8 failure code must fail closed to empty")

	exact := EffectResult{
		Port:         "complete",
		FailureCode:  "child-item.failed",
		FailureStage: "reducer",
		FailurePath:  "$.input.reviewers[0]",
	}
	normalized = exact.Normalize()
	require.Equal(t, "complete", normalized.Port)
	require.Equal(t, "child-item.failed", normalized.FailureCode)
	require.Equal(t, "reducer", normalized.FailureStage)
	require.Equal(t, "$.input.reviewers[0]", normalized.FailurePath)
}
