package orchestrator

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestEffectProtocolRoundTripAndStableIDs(t *testing.T) {
	now := time.Date(2026, 7, 21, 1, 2, 3, 456789000, time.UTC)
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "notify",
			Callback: true, CallbackOutput: nil,
			Retry: plan.ArtifactRetryPolicy{MaxAttempts: 2, BackoffNanos: int64(time.Second)},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
			ActivationID: "a1", EffectID: "e", NodeID: "notify", SourceKind: "manual",
			OwnerUserID: "u", AuthorMemberID: "m", TargetRef: "#loop", Content: "@agent work",
			ClientMessageID: "workflow-run:e", Callback: true,
		},
	}
	payload, err := EncodeEffectRequest(request)
	require.NoError(t, err)
	decoded, err := DecodeEffectRequest(payload)
	require.NoError(t, err)
	require.Equal(t, request, decoded)

	// Round-trip a authoritative effect result; the exact port survives unchanged and
	// the free-text callback responder agent id and wake-agent list normalize.
	resultPayload, err := EncodeEffectResult(EffectResult{
		Port: "completed", Output: run.Values{"reply": "ok"},
		WakeAgentIDs: []string{"b", "a", "b"}, CallbackResponderAgentID: " b ", CompletedAt: &now,
	})
	require.NoError(t, err)
	result, err := DecodeEffectResult(resultPayload)
	require.NoError(t, err)
	require.Equal(t, "completed", result.Port)
	require.Equal(t, []string{"a", "b"}, result.WakeAgentIDs)
	require.Equal(t, "b", result.CallbackResponderAgentID)
	completion, err := result.Completion("a1")
	require.NoError(t, err)
	require.Equal(t, run.Values{"reply": "ok"}, completion.Output)
	require.Nil(t, completion.Failure)
	require.Equal(t, "b", completion.CallbackResponderAgentID)

	// A spaced port is a malformed identity and fails closed to empty; it is
	// never trimmed into a valid port.
	spacedPayload, err := EncodeEffectResult(EffectResult{Port: " completed "})
	require.NoError(t, err)
	spaced, err := DecodeEffectResult(spacedPayload)
	require.NoError(t, err)
	require.Empty(t, spaced.Port, "a spaced port must fail closed (never trimmed into a valid port)")

	failureCompletion, err := (EffectResult{
		Port: "failed", FailureCode: "message.delivery_failed", FailureMessage: "delivery exhausted",
	}).Completion("a1")
	require.NoError(t, err)
	require.Equal(t, &run.Failure{Code: "message.delivery_failed", Message: "delivery exhausted"}, failureCompletion.Failure)

	first, err := StableChildID("wra", "run-one", "a1")
	require.NoError(t, err)
	again, err := StableChildID("wra", "run-one", "a1")
	require.NoError(t, err)
	other, err := StableChildID("wra", "run-two", "a1")
	require.NoError(t, err)
	require.Equal(t, first, again)
	require.NotEqual(t, first, other)
	require.Len(t, first, 36)

	key, err := StableEffectIdempotencyKey("run-one", "a1", run.IntentSendMessage)
	require.NoError(t, err)
	require.Len(t, key, len("workflow-run:")+64)
}

func TestEffectProtocolFreezesWorkItemsDependencyValueContract(t *testing.T) {
	contract := plan.ArtifactContract{{
		Name: "result",
		Schema: plan.ArtifactSchema{
			Type: plan.TypeNumber, Required: true, Values: []string{},
		},
	}}
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "planner",
			Callback: true, WorkItemsAllowedResponders: []string{"@calculator"},
			WorkItemsDependencyValueContract: &contract,
			Retry:                            plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: int64(time.Second)},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
			ActivationID: "a1", EffectID: "e", NodeID: "planner", SourceKind: "manual",
			OwnerUserID: "u", AuthorMemberID: "m", TargetRef: "#loop", Content: "plan",
			ClientMessageID: "workflow-run:e", Callback: true,
		},
	}
	payload, err := EncodeEffectRequest(request)
	require.NoError(t, err)
	decoded, err := DecodeEffectRequest(payload)
	require.NoError(t, err)
	require.Equal(t, request, decoded)

	missingContract := request
	missingContract.Intent.WorkItemsDependencyValueContract = nil
	_, err = EncodeEffectRequest(missingContract)
	require.ErrorContains(t, err, "requires both responder and dependency value contracts")

	missingResponders := request
	missingResponders.Intent.WorkItemsAllowedResponders = nil
	_, err = EncodeEffectRequest(missingResponders)
	require.ErrorContains(t, err, "requires both responder and dependency value contracts")
}

func TestCallbackResponderAuthoritySupportsTaskWatchWakeSetsAndLegacyReplay(t *testing.T) {
	authority, err := ResolveCallbackResponderAuthority(
		true,
		"agt_exact",
		[]string{"agt_watcher", "agt_exact", "agt_assignee"},
	)
	require.NoError(t, err)
	require.Equal(t, "agt_exact", authority)

	authority, err = ResolveCallbackResponderAuthority(true, "", []string{"agt_legacy"})
	require.NoError(t, err)
	require.Equal(t, "agt_legacy", authority)

	_, err = ResolveCallbackResponderAuthority(true, "", []string{"agt_a", "agt_b"})
	require.EqualError(t, err, "legacy workflow Run callback receipt must wake exactly one agent")
	_, err = ResolveCallbackResponderAuthority(true, "agt_missing", []string{"agt_watcher"})
	require.EqualError(t, err, "workflow Run callback responder authority must be present in the wake set")
}

func TestEffectProtocolRoundTripsImmutableTaskWatchHandoff(t *testing.T) {
	taskScope := domain.MessageScopeValue{Kind: domain.MessageScopeKindTask, ScopeID: "tsk_shared"}
	handoff := &run.TaskWatchHandoff{
		TaskScope: taskScope, WorkItemID: "wfi_child",
		CausalPredecessorAgentIDs: []string{"agt_root", "agt_middle"},
	}
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "implement",
			Scope: &taskScope, Responder: "@dev", Callback: true,
			WorkScopeMode: run.WorkScopeModeFixed, WorkScope: &taskScope,
			TaskWatchHandoff: handoff,
			Retry:            plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
			ActivationID: "a1", EffectID: "e", NodeID: "implement", SourceKind: "manual",
			OwnerUserID: "u", AuthorMemberID: "m", Scope: &taskScope, Content: "work",
			ClientMessageID: "workflow-run:e", Callback: true, ResponderAgentRef: "@dev",
			WorkScopeMode: run.WorkScopeModeFixed, WorkScope: &taskScope,
			TaskWatchHandoff: handoff,
		},
	}
	payload, err := EncodeEffectRequest(request)
	require.NoError(t, err)
	decoded, err := DecodeEffectRequest(payload)
	require.NoError(t, err)
	require.Equal(t, request, decoded)

	decoded.Message.TaskWatchHandoff.CausalPredecessorAgentIDs = append(
		decoded.Message.TaskWatchHandoff.CausalPredecessorAgentIDs,
		"agt_other",
	)
	require.ErrorContains(t, decoded.Validate(), "does not match reducer intent")
}

func TestEffectProtocolRejectsMismatchedOrUnknownRequests(t *testing.T) {
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "n",
			Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r", ActivationID: "other",
			EffectID: "e", NodeID: "n", SourceKind: "manual", OwnerUserID: "u", AuthorMemberID: "m",
			TargetRef: "#loop", Content: "hi", ClientMessageID: "id",
		},
	}
	require.ErrorContains(t, request.Validate(), "does not match")
	_, err := DecodeEffectRequest([]byte(`{"intent":{},"unexpected":true}`))
	require.ErrorContains(t, err, "unknown field")
}

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

	scope := domain.MessageScopeValue{
		Kind: domain.MessageScopeKindTask, ScopeID: "tsk_abcdefghijklmn",
	}
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "notify",
			Scope: &scope,
			Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
			ActivationID: "a1", EffectID: "e", NodeID: "notify", SourceKind: "manual",
			OwnerUserID: "u", AuthorMemberID: "m", Scope: &scope, Content: "work",
			ClientMessageID: "workflow-run:e",
		},
	}
	payload, err := EncodeEffectRequest(request)
	require.NoError(t, err)
	decoded, err := DecodeEffectRequest(payload)
	require.NoError(t, err)
	require.Equal(t, request, decoded)

	other := domain.MessageScopeValue{
		Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn",
	}
	request.Message.Scope = &other
	require.ErrorContains(t, request.Validate(), "does not match reducer intent")
}

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

	workScope := domain.MessageScopeValue{Kind: domain.MessageScopeKindTask, ScopeID: "tsk_work"}
	relatedScopes := []domain.MessageScopeValue{
		{Kind: domain.MessageScopeKindThread, ScopeID: "thr_requirements"},
		{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_design"},
	}
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "implement",
			Callback: true, WorkScopeMode: run.WorkScopeModeFixed, WorkScope: &workScope,
			RelatedScopes: relatedScopes,
			Retry:         plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
			ActivationID: "a1", EffectID: "e", NodeID: "implement", SourceKind: "manual",
			OwnerUserID: "u", AuthorMemberID: "m", TargetRef: "#loop", Content: "@agent implement",
			ClientMessageID: "workflow-run:e", Callback: true,
			WorkScopeMode: run.WorkScopeModeFixed, WorkScope: &workScope, RelatedScopes: relatedScopes,
		},
	}
	payload, err := EncodeEffectRequest(request)
	require.NoError(t, err)
	decoded, err := DecodeEffectRequest(payload)
	require.NoError(t, err)
	require.Equal(t, request, decoded)

	request.Message.RelatedScopes = append([]domain.MessageScopeValue(nil), relatedScopes[:1]...)
	require.ErrorContains(t, request.Validate(), "does not match reducer intent")
	request.Message.RelatedScopes = append([]domain.MessageScopeValue(nil), relatedScopes...)
	request.Message.RelatedScopes = append(request.Message.RelatedScopes, relatedScopes[0])
	request.Intent.RelatedScopes = append(request.Intent.RelatedScopes, relatedScopes[0])
	require.ErrorContains(t, request.Validate(), "relatedScopes[2] is duplicated")
}

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

	relatedScopes := make([]domain.MessageScopeValue, plan.MaxSendMessageRelatedScopes)
	for index := range relatedScopes {
		relatedScopes[index] = domain.MessageScopeValue{
			Kind:    domain.MessageScopeKindTask,
			ScopeID: fmt.Sprintf("tsk_context_%02d", index),
		}
	}
	request := EffectRequest{
		Intent: run.Intent{
			Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "implement",
			Callback: true, WorkScopeMode: run.WorkScopeModeAgent,
			RelatedScopes: relatedScopes,
			Retry:         plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		},
		Message: &MessageRequest{
			WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
			ActivationID: "a1", EffectID: "e", NodeID: "implement", SourceKind: "manual",
			OwnerUserID: "u", AuthorMemberID: "m", TargetRef: "#loop", Content: "@agent implement",
			ClientMessageID: "workflow-run:e", Callback: true,
			WorkScopeMode: run.WorkScopeModeAgent, RelatedScopes: relatedScopes,
		},
	}
	payload, err := EncodeEffectRequest(request)
	require.NoError(t, err)
	decoded, err := DecodeEffectRequest(payload)
	require.NoError(t, err)
	require.Equal(t, request, decoded)

	overflow := append(
		append([]domain.MessageScopeValue(nil), relatedScopes...),
		domain.MessageScopeValue{Kind: domain.MessageScopeKindTask, ScopeID: "tsk_context_64"},
	)
	request.Intent.RelatedScopes = overflow
	request.Message.RelatedScopes = append([]domain.MessageScopeValue(nil), overflow...)
	require.ErrorContains(
		t,
		request.Message.Validate(),
		fmt.Sprintf("more than %d scopes", plan.MaxSendMessageRelatedScopes),
	)
	require.ErrorContains(
		t,
		request.Validate(),
		fmt.Sprintf("more than %d scopes", plan.MaxSendMessageRelatedScopes),
	)
}

func ptrTime(value time.Time) *time.Time { return &value }

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

	when := time.Date(2026, 8, 1, 1, 0, 0, 0, time.UTC)
	baseMessage := &MessageRequest{
		WorkspaceID: "w", PlanID: "p", PlanVersionID: "pv", RunID: "r",
		ActivationID: "a1", EffectID: "e", NodeID: "n", SourceKind: "manual",
		OwnerUserID: "u", AuthorMemberID: "m", TargetRef: "#loop", Content: "work",
		ClientMessageID: "workflow-run:e",
	}

	t.Run("send_message deadline", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "n",
				Deadline: ptrTime(when), Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
			Message: baseMessage,
		}
		require.ErrorContains(t, request.Validate(), "send_message effect cannot carry a deadline")
		_, err := EncodeEffectRequest(request)
		require.ErrorContains(t, err, "cannot carry a deadline")
	})

	t.Run("send_message wakeAt", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "n",
				WakeAt: ptrTime(when), Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
			Message: baseMessage,
		}
		require.ErrorContains(t, request.Validate(), "send_message effect cannot carry wakeAt")
	})

	t.Run("send_message zero deadline pointer", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "n",
				Deadline: ptrTime(time.Time{}), Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
			Message: baseMessage,
		}
		require.ErrorContains(t, request.Validate(), "send_message effect cannot carry a deadline")
		_, err := EncodeEffectRequest(request)
		require.ErrorContains(t, err, "cannot carry a deadline")
	})

	t.Run("send_message zero wakeAt pointer", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentSendMessage, ActivationID: "a1", NodeID: "n",
				WakeAt: ptrTime(time.Time{}), Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
			Message: baseMessage,
		}
		require.ErrorContains(t, request.Validate(), "send_message effect cannot carry wakeAt")
	})

	t.Run("request_approval deadline", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentApproval, ActivationID: "a1", NodeID: "n",
				Assignee: "@alice", Summary: "Approve", Deadline: ptrTime(when),
				Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
		}
		require.ErrorContains(t, request.Validate(), "request_approval effect cannot carry a deadline")
	})

	t.Run("request_approval wakeAt", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentApproval, ActivationID: "a1", NodeID: "n",
				Assignee: "@alice", Summary: "Approve", WakeAt: ptrTime(when),
				Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
		}
		require.ErrorContains(t, request.Validate(), "request_approval effect cannot carry wakeAt")
	})

	t.Run("request_approval zero deadline pointer", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentApproval, ActivationID: "a1", NodeID: "n",
				Assignee: "@alice", Summary: "Approve", Deadline: ptrTime(time.Time{}),
				Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
		}
		require.ErrorContains(t, request.Validate(), "request_approval effect cannot carry a deadline")
	})

	t.Run("request_approval zero wakeAt pointer", func(t *testing.T) {
		request := EffectRequest{
			Intent: run.Intent{
				Kind: run.IntentApproval, ActivationID: "a1", NodeID: "n",
				Assignee: "@alice", Summary: "Approve", WakeAt: ptrTime(time.Time{}),
				Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
			},
		}
		require.ErrorContains(t, request.Validate(), "request_approval effect cannot carry wakeAt")
	})

	t.Run("wait keeps timing", func(t *testing.T) {
		request := EffectRequest{Intent: run.Intent{
			Kind: run.IntentWait, ActivationID: "a1", NodeID: "n",
			WakeAt: ptrTime(when), Deadline: ptrTime(when.Add(time.Minute)),
			Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		}}
		require.NoError(t, request.Validate())
	})

	t.Run("wait rejects zero deadline", func(t *testing.T) {
		request := EffectRequest{Intent: run.Intent{
			Kind: run.IntentWait, ActivationID: "a1", NodeID: "n",
			WakeAt: ptrTime(when), Deadline: ptrTime(time.Time{}),
			Retry: plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		}}
		require.ErrorContains(t, request.Validate(), "timer effect requires a deadline")
	})

	t.Run("wait rejects missing wakeAt", func(t *testing.T) {
		request := EffectRequest{Intent: run.Intent{
			Kind: run.IntentWait, ActivationID: "a1", NodeID: "n",
			Deadline: ptrTime(when.Add(time.Minute)),
			Retry:    plan.ArtifactRetryPolicy{MaxAttempts: 1, BackoffNanos: 1},
		}}
		require.ErrorContains(t, request.Validate(), "timer effect requires wakeAt and no message request")
	})
}
