package fanout

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/unread"
)

type taskScopeLifecycleGuardStub struct {
	active        bool
	err           error
	callbackCalls int
	calls         []taskScopeLifecycleGuardCall
}

type taskScopeLifecycleGuardCall struct {
	workspaceID string
	scopeID     string
}

func (guard *taskScopeLifecycleGuardStub) WithActiveTaskScopes(
	_ context.Context,
	workspaceID string,
	scopeIDs []string,
	fanout func() error,
) (bool, error) {
	for _, scopeID := range scopeIDs {
		guard.calls = append(guard.calls, taskScopeLifecycleGuardCall{
			workspaceID: workspaceID,
			scopeID:     scopeID,
		})
	}
	if guard.err != nil {
		return false, guard.err
	}
	if !guard.active {
		return false, nil
	}
	guard.callbackCalls++
	return true, fanout()
}

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

	guardErr := errors.New("lifecycle unavailable")
	tests := []struct {
		name      string
		guard     *taskScopeLifecycleGuardStub
		wantError error
	}{
		{
			name:  "deleted Task",
			guard: &taskScopeLifecycleGuardStub{},
		},
		{
			name:      "guard failure",
			guard:     &taskScopeLifecycleGuardStub{err: guardErr},
			wantError: guardErr,
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			notifier := &capturingNotifier{}
			realtime := &fakeRealtime{}
			unread := &capturingUnreadRuntime{deltas: []unread.UnreadDelta{{
				WorkspaceID:  "wsp_task_lifecycle",
				MemberID:     "mem_reader",
				ScopeID:      "tsk_task_lifecycle",
				ScopeType:    unread.ScopeTypeTask,
				Count:        1,
				HasUnread:    true,
				ServingState: unread.ServingStateReady,
			}}}
			deliveryFanout := NewDeliveryFanout(notifier, realtime, nil).
				WithTaskScopeLifecycleGuard(test.guard).
				WithUnreadRuntime(unread).
				WithUnreadDeltaSink(realtime)

			err := deliveryFanout.DeliverCommittedMessage(
				t.Context(),
				"wsp_task_lifecycle",
				taskLifecycleAppMessage("tsk_task_lifecycle"),
				taskLifecycleDelivery("tsk_task_lifecycle"),
			)
			if test.wantError == nil {
				require.NoError(t, err)
			} else {
				require.ErrorIs(t, err, test.wantError)
			}

			require.Equal(t, []taskScopeLifecycleGuardCall{{
				workspaceID: "wsp_task_lifecycle",
				scopeID:     "tsk_task_lifecycle",
			}}, test.guard.calls)
			require.Zero(t, test.guard.callbackCalls)
			require.Empty(t, unread.messages, "deleted Task must not regain unread state")
			require.Empty(t, realtime.messageNew, "deleted Task must not emit a browser stream event")
			require.Empty(t, realtime.unreadUpdated, "deleted Task must not emit an unread delta")
			require.Empty(t, notifier.messages, "deleted Task must not wake an agent")
		})
	}
}

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

	guard := &taskScopeLifecycleGuardStub{}
	notifier := &capturingNotifier{}
	realtime := &fakeRealtime{}
	unread := &capturingUnreadRuntime{deltas: []unread.UnreadDelta{{
		WorkspaceID:  "wsp_channel_lifecycle",
		MemberID:     "mem_reader",
		ScopeID:      "chn_channel_lifecycle",
		ScopeType:    unread.ScopeTypeChannel,
		Count:        1,
		HasUnread:    true,
		ServingState: unread.ServingStateReady,
	}}}
	deliveryFanout := NewDeliveryFanout(notifier, realtime, nil).
		WithTaskScopeLifecycleGuard(guard).
		WithUnreadRuntime(unread).
		WithUnreadDeltaSink(realtime)

	appMessage := taskLifecycleAppMessage("chn_channel_lifecycle")
	appMessage.ChannelType = message.ConversationKindChannel
	delivery := taskLifecycleDelivery("chn_channel_lifecycle")
	delivery.Message.ChannelType = message.ConversationKindChannel

	require.NoError(t, deliveryFanout.DeliverCommittedMessage(
		t.Context(),
		"wsp_channel_lifecycle",
		appMessage,
		delivery,
	))

	require.Empty(t, guard.calls)
	require.Zero(t, guard.callbackCalls)
	require.Len(t, unread.messages, 1)
	require.Equal(t, []string{"chn_channel_lifecycle"}, realtime.messageNew)
	require.Len(t, realtime.unreadUpdated, 1)
	require.Len(t, notifier.messages, 1)
}

func taskLifecycleAppMessage(scopeID string) message.AppMessage {
	return message.AppMessage{
		ID:          "msg_task_lifecycle",
		Seq:         7,
		ChannelID:   scopeID,
		ChannelType: message.ConversationKindTask,
		AuthorProfile: message.AuthorProfile{
			ActorType:   message.AuthorActorTypeHuman,
			MemberID:    "mem_author",
			DisplayName: "Alice",
		},
		MessageType: message.MessageTypeTaskDiscussion,
		Content:     "please review",
	}
}

func taskLifecycleDelivery(scopeID string) message.MessageDelivery {
	return message.MessageDelivery{
		MessageID:               "msg_task_lifecycle",
		Seq:                     7,
		HumanRecipientMemberIDs: []string{"mem_reader"},
		HumanRecipientsTargeted: true,
		Message: message.ReceiveMessage{
			MessageID:   "msg_task_lifecycle",
			TargetKey:   scopeID,
			TargetSeq:   7,
			ChannelType: message.ConversationKindTask,
			Content:     "please review",
		},
		WakeRecipients: []message.AgentRecipient{{
			AgentID:         "agt_reviewer",
			MachineID:       "mac_reviewer",
			AutoWakeEnabled: true,
		}},
	}
}
