package channeladmin

import (
	"context"
	"errors"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"

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

type recordingMessageFanout struct {
	mu        sync.Mutex
	projected int
}

func (f *recordingMessageFanout) DeliverCommittedMessage(context.Context, string, message.AppMessage, message.MessageDelivery) error {
	return nil
}

func (f *recordingMessageFanout) EmitCommittedMessageProjectionOnly(context.Context, string, message.AppMessage, message.MessageDelivery) {
	f.mu.Lock()
	f.projected++
	f.mu.Unlock()
}

type recordingUnread struct {
	mu          sync.Mutex
	invalidated []string
	deltas      []unreadservice.UnreadDelta
	err         error
}

func (u *recordingUnread) ApplyScopeInvalidatedWithDeltas(event unreadservice.ScopeInvalidated) ([]unreadservice.UnreadDelta, error) {
	u.mu.Lock()
	u.invalidated = append(u.invalidated, event.ScopeID)
	deltas := append([]unreadservice.UnreadDelta(nil), u.deltas...)
	err := u.err
	u.mu.Unlock()
	return deltas, err
}

type recordingRoomLister struct {
	mu      sync.Mutex
	calls   int
	roomIDs []string
	listErr error
}

func (l *recordingRoomLister) ListRealtimeRoomIDsForChannel(context.Context, string, string) ([]string, error) {
	l.mu.Lock()
	l.calls++
	l.mu.Unlock()
	return l.roomIDs, l.listErr
}

type recordingChannelAppStore struct {
	ChannelAppStore
	roomLister *recordingRoomLister
}

func (store *recordingChannelAppStore) ListRealtimeRoomIDsForChannel(ctx context.Context, workspaceID string, channelID string) ([]string, error) {
	return store.roomLister.ListRealtimeRoomIDsForChannel(ctx, workspaceID, channelID)
}

// TestEmitChannelMemberMutationEffectsCleanupOnlyFansOut locks down that a
// cleanup-only mutation (real cascade MembershipChanges, no direct system message
// and no human subscription) still fans out: unread invalidation + agent wake
// clear + members/channel updated all run; projection does not (no EventResults).
func TestEmitChannelMemberMutationEffectsCleanupOnlyFansOut(t *testing.T) {
	ctx := context.Background()
	realtime := &recordingRealtime{}
	fanout := &recordingMessageFanout{}
	unread := &recordingUnread{}
	deps := Dependencies{Realtime: realtime, MessageFanout: fanout, Unread: unread}
	effects := &app.ChannelMemberMutationEffects{
		MembershipChanges: []app.ChannelMembershipChange{
			{ScopeID: "chn_parent", MemberID: "mem_agent", Kind: message.ChannelMemberEventTargetAgent, Action: message.ChannelMemberEventActionRemoved, AgentID: "agt_x"},
			{ScopeID: "thr_child", MemberID: "mem_agent", Kind: message.ChannelMemberEventTargetAgent, Action: message.ChannelMemberEventActionRemoved, AgentID: "agt_x"},
		},
	}
	require.True(t, effects.HasChanges(), "cleanup-only with real cascade changes must report HasChanges")

	emitChannelMemberMutationEffects(ctx, deps, "ws", "chn_parent", effects)

	require.Equal(t, []string{"chn_parent", "thr_child"}, unread.invalidated, "unread invalidated per changed scope/member")
	require.Equal(t, []wakeClear{{scopeID: "chn_parent", agentID: "agt_x"}, {scopeID: "thr_child", agentID: "agt_x"}}, realtime.wakeClears, "agent wake cleared per changed agent scope on remove")
	require.Equal(t, 0, fanout.projected, "no projection without EventResults")
	require.Equal(t, 1, realtime.membersUpdated)
	require.Equal(t, 1, realtime.channelUpdated)
	require.Empty(t, realtime.subscribedRooms)
	require.Empty(t, realtime.unsubscribedRooms)
}

// TestEmitChannelMemberMutationEffectsWakeClearOnlyOnRemove locks down that
// agent wake is cleared only on member removal, never on add, and the clear is
// scoped to the exact changed scope (parent + child).
func TestEmitChannelMemberMutationEffectsWakeClearOnlyOnRemove(t *testing.T) {
	ctx := context.Background()
	t.Run("add does not clear wake", func(t *testing.T) {
		realtime := &recordingRealtime{}
		effects := &app.ChannelMemberMutationEffects{
			MembershipChanges: []app.ChannelMembershipChange{
				{ScopeID: "chn", MemberID: "mem_agent", Kind: message.ChannelMemberEventTargetAgent, Action: message.ChannelMemberEventActionAdded, AgentID: "agt_x"},
			},
		}
		emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime}, "ws", "chn", effects)
		require.Empty(t, realtime.wakeClears, "agent add must not clear wake")
	})
	t.Run("remove clears wake on exact scope", func(t *testing.T) {
		realtime := &recordingRealtime{}
		effects := &app.ChannelMemberMutationEffects{
			MembershipChanges: []app.ChannelMembershipChange{
				{ScopeID: "chn_parent", MemberID: "mem_agent", Kind: message.ChannelMemberEventTargetAgent, Action: message.ChannelMemberEventActionRemoved, AgentID: "agt_x"},
				{ScopeID: "thr_child", MemberID: "mem_agent", Kind: message.ChannelMemberEventTargetAgent, Action: message.ChannelMemberEventActionRemoved, AgentID: "agt_x"},
			},
		}
		emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime}, "ws", "chn_parent", effects)
		require.Equal(t, []wakeClear{{scopeID: "chn_parent", agentID: "agt_x"}, {scopeID: "thr_child", agentID: "agt_x"}}, realtime.wakeClears)
	})
}

func TestEmitChannelMemberMutationEffectsEmitsUnreadDeltasOnlyOnSuccessfulInvalidation(t *testing.T) {
	ctx := context.Background()
	delta := unreadservice.UnreadDelta{
		WorkspaceID: "ws", UserID: "usr_1", MemberID: "mem_1",
		ScopeID: "chn", Count: 3, HasUnread: true, StateVersion: 7,
	}
	effects := &app.ChannelMemberMutationEffects{
		MembershipChanges: []app.ChannelMembershipChange{{
			ScopeID: "chn", MemberID: "mem_1", Kind: message.ChannelMemberEventTargetHuman,
			Action: message.ChannelMemberEventActionRemoved, UserID: "usr_1",
		}},
	}

	t.Run("non-empty delta emitted exactly once", func(t *testing.T) {
		realtime := &recordingRealtime{}
		unread := &recordingUnread{deltas: []unreadservice.UnreadDelta{delta}}

		emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime, Unread: unread}, "ws", "chn", effects)

		require.Equal(t, []string{"chn"}, unread.invalidated)
		require.Equal(t, []unreadservice.UnreadDelta{delta}, realtime.unreadUpdated)
	})

	t.Run("invalidation error emits no delta", func(t *testing.T) {
		realtime := &recordingRealtime{}
		unread := &recordingUnread{deltas: []unreadservice.UnreadDelta{delta}, err: errors.New("unread failed")}

		emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime, Unread: unread}, "ws", "chn", effects)

		require.Equal(t, []string{"chn"}, unread.invalidated)
		require.Empty(t, realtime.unreadUpdated)
	})
}

// TestEmitChannelMemberMutationEffectsNilGatesEachPort locks down that each port
// nil-gates independently: a missing dependency does not swallow the others.
func TestEmitChannelMemberMutationEffectsNilGatesEachPort(t *testing.T) {
	ctx := context.Background()
	projectionEffects := &app.ChannelMemberMutationEffects{
		EventResults: []message.ChannelMemberEventResult{{Changed: true, Message: message.AppMessage{}, Delivery: message.MessageDelivery{}}},
		MembershipChanges: []app.ChannelMembershipChange{
			{ScopeID: "chn", MemberID: "mem_agent", Kind: message.ChannelMemberEventTargetAgent, Action: message.ChannelMemberEventActionRemoved, AgentID: "agt_x"},
		},
	}

	t.Run("message fanout nil does not block unread/realtime", func(t *testing.T) {
		realtime := &recordingRealtime{}
		unread := &recordingUnread{}
		emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime, Unread: unread}, "ws", "chn", projectionEffects)
		require.Equal(t, []string{"chn"}, unread.invalidated)
		require.Equal(t, []wakeClear{{scopeID: "chn", agentID: "agt_x"}}, realtime.wakeClears)
		require.Equal(t, 1, realtime.membersUpdated)
	})
	t.Run("unread nil does not block projection/realtime", func(t *testing.T) {
		realtime := &recordingRealtime{}
		fanout := &recordingMessageFanout{}
		emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime, MessageFanout: fanout}, "ws", "chn", projectionEffects)
		require.Equal(t, 1, fanout.projected)
		require.Equal(t, []wakeClear{{scopeID: "chn", agentID: "agt_x"}}, realtime.wakeClears)
		require.Equal(t, 1, realtime.membersUpdated)
	})
	t.Run("realtime nil does not block projection/unread", func(t *testing.T) {
		fanout := &recordingMessageFanout{}
		unread := &recordingUnread{}
		emitChannelMemberMutationEffects(ctx, Dependencies{MessageFanout: fanout, Unread: unread}, "ws", "chn", projectionEffects)
		require.Equal(t, 1, fanout.projected)
		require.Equal(t, []string{"chn"}, unread.invalidated)
	})
}

// TestEmitChannelMemberMutationEffectsHumanRemoveUsesCapturedRooms locks down
// that the Agent Exec path consumes the stable room set captured in the
// mutation transaction and never performs a post-commit topology read.
func TestEmitChannelMemberMutationEffectsHumanRemoveUsesCapturedRooms(t *testing.T) {
	ctx := context.Background()
	realtime := &recordingRealtime{}
	roomLister := &recordingRoomLister{roomIDs: []string{"chn_wrong", "thr_wrong"}}
	deps := Dependencies{
		Apps:          &recordingChannelAppStore{roomLister: roomLister},
		Realtime:      realtime,
		MessageFanout: &recordingMessageFanout{},
		Unread:        &recordingUnread{},
	}
	effects := &app.ChannelMemberMutationEffects{
		HumanSubscriptions: []app.ChannelHumanSubscription{{UserID: "usr_human", Action: message.ChannelMemberEventActionRemoved}},
		RealtimeRoomIDs:    []string{"chn_parent", "thr_child"},
	}
	emitChannelMemberMutationEffects(ctx, deps, "ws", "chn_parent", effects)
	require.Equal(t, []string{"chn_parent", "thr_child"}, realtime.unsubscribedRooms)
	require.Zero(t, roomLister.calls, "post-commit fanout must not re-read room topology")
}

func TestEmitChannelMemberMutationEffectsHumanRemoveDoesNotFallbackWhenCapturedRoomsMissing(t *testing.T) {
	ctx := context.Background()
	realtime := &recordingRealtime{}
	roomLister := &recordingRoomLister{roomIDs: []string{"chn_parent", "thr_child"}}
	deps := Dependencies{
		Apps:     &recordingChannelAppStore{roomLister: roomLister},
		Realtime: realtime,
	}
	effects := &app.ChannelMemberMutationEffects{
		HumanSubscriptions: []app.ChannelHumanSubscription{{UserID: "usr_human", Action: message.ChannelMemberEventActionRemoved}},
	}

	emitChannelMemberMutationEffects(ctx, deps, "ws", "chn_parent", effects)

	require.Empty(t, realtime.unsubscribedRooms)
	require.Zero(t, roomLister.calls, "an invalid empty captured set must not trigger a fail-open post-commit read")
}

func TestEmitChannelMemberMutationEffectsUnknownSubscriptionActionDoesNotRevokeRooms(t *testing.T) {
	ctx := context.Background()
	realtime := &recordingRealtime{}
	effects := &app.ChannelMemberMutationEffects{
		HumanSubscriptions: []app.ChannelHumanSubscription{{
			UserID: "usr_human",
			Action: "updated",
		}},
		RealtimeRoomIDs: []string{"chn_parent", "thr_child"},
	}

	emitChannelMemberMutationEffects(ctx, Dependencies{Realtime: realtime}, "ws", "chn_parent", effects)

	require.Empty(t, realtime.subscribedRooms)
	require.Empty(t, realtime.unsubscribedRooms, "only an explicit removed action may revoke captured rooms")
}
