package channel

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

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

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

	store := &channelHandlerTestStore{listRooms: []string{"chn_1:root", "chn_1:thread"}}
	realtime := &channelHandlerTestRealtime{}
	unread := &fakeChannelUnreadReader{workspaceHasUnread: true}
	handler := &ChannelMembershipHandler{channels: store, appStore: store, realtime: realtime, unreadRuntime: unread}

	joinResponse, err := handler.Join(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "chn_1", store.joined.channelID)
	require.Equal(t, dto.EmptyResponse{}, joinResponse.Body)
	require.Equal(t, []string{"wsp_1"}, realtime.updated)
	require.Equal(t, []unreadservice.ScopeInvalidated{{
		WorkspaceID: "wsp_1",
		ScopeID:     "chn_1",
		MemberIDs:   []string{"mem_1"},
		Reason:      "channel_join",
	}}, unread.invalidated)
	require.Equal(t, []unreadservice.UnreadDelta{
		channelSuppressedUnreadDelta("chn_1", true),
	}, realtime.unreadUpdated)

	leaveResponse, err := handler.Leave(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "chn_1", store.left.channelID)
	require.Equal(t, dto.EmptyResponse{}, leaveResponse.Body)
	require.Equal(t, []string{"wsp_1:usr_1:chn_1", "wsp_1:usr_1:chn_1:root", "wsp_1:usr_1:chn_1:thread"}, realtime.scopeUnsubscribed)
	require.Equal(t, []string{"wsp_1", "wsp_1"}, realtime.updated)
	require.Equal(t, unreadservice.ScopeInvalidated{
		WorkspaceID: "wsp_1",
		ScopeID:     "chn_1",
		MemberIDs:   []string{"mem_1"},
		Reason:      "channel_leave",
	}, unread.invalidated[1])
	require.Equal(t, []unreadservice.UnreadDelta{
		channelSuppressedUnreadDelta("chn_1", true),
		channelSuppressedUnreadDelta("chn_1", true),
	}, realtime.unreadUpdated)

	membersResponse, err := handler.GetMembers(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "chn_1", store.membersForUser.channelID)
	require.Equal(t, dto.ChannelMemberList{Agents: []dto.ChannelMemberSummary{}, Humans: []dto.ChannelMemberSummary{{ID: "usr_2", Name: "alice"}}, Services: []dto.ChannelMemberSummary{}, PrivateAgents: []dto.ChannelPrivateAgentSummary{}}, membersResponse.Body)
}

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

	store := &channelHandlerTestStore{}
	handler := &ChannelMembershipHandler{appStore: store}
	request := channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"})
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/?q=Agent&limit=5", nil)

	response, err := handler.AddableAgents(request)
	require.NoError(t, err)
	require.Equal(t, "usr_1", store.addableAgents.userID)
	require.Equal(t, "wsp_1", store.addableAgents.workspaceID)
	require.Equal(t, "chn_1", store.addableAgents.channelID)
	require.Equal(t, "Agent", store.addableAgents.options.Query)
	require.Equal(t, 5, store.addableAgents.options.Limit)
	require.Equal(t, dto.AgentDirectoryResponse{
		Agents: []dto.AgentDirectoryItem{{
			ID:          "agt_1",
			Name:        "agent-one",
			MemberID:    "mem_agt_1",
			DisplayName: "Agent One",
			AvatarURL:   "https://example.com/agent.png",
			OwnerUserID: "usr_1",
			MachineID:   "mac_1",
			CanManage:   true,
		}},
		Limit:   5,
		HasMore: true,
	}, response.Body)

	invalid := channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"})
	invalid.Gin.Request = httptest.NewRequest(http.MethodGet, "/?limit=0", nil)
	_, err = handler.AddableAgents(invalid)
	requireHTTPError(t, err, http.StatusBadRequest, "limit must be positive")
}

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

	store := &channelHandlerTestStore{}
	handler := &ChannelMembershipHandler{appStore: store}
	request := channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"})
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/?q=Alice&limit=5&cursor=person_cursor", nil)

	response, err := handler.AddablePeople(request)
	require.NoError(t, err)
	require.Equal(t, "usr_1", store.addablePeople.userID)
	require.Equal(t, "wsp_1", store.addablePeople.workspaceID)
	require.Equal(t, "chn_1", store.addablePeople.channelID)
	require.Equal(t, "Alice", store.addablePeople.options.Query)
	require.Equal(t, 5, store.addablePeople.options.Limit)
	require.Equal(t, "person_cursor", store.addablePeople.options.Cursor)
	require.Equal(t, dto.ChannelAddablePeopleResponse{
		Humans: []dto.WorkspaceMemberResponse{{
			UserID:      "usr_2",
			Email:       "alice@example.com",
			Name:        "alice",
			DisplayName: "Alice",
			MemberID:    "mem_usr_2",
			Role:        "member",
		}},
		Services: []dto.AppInstallationResponse{{
			ID:       "ainst_1",
			AppID:    "app_1",
			AppSlug:  "demo",
			AppName:  "Demo",
			MemberID: "mem_app_1",
			Handle:   "demo",
			Enabled:  true,
		}},
		HasMore:    true,
		NextCursor: "next_people",
	}, response.Body)

	invalid := channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"})
	invalid.Gin.Request = httptest.NewRequest(http.MethodGet, "/?limit=0", nil)
	_, err = handler.AddablePeople(invalid)
	requireHTTPError(t, err, http.StatusBadRequest, "limit must be positive")
}

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

	store := &channelHandlerTestStore{listRoomsErr: errors.New("boom")}
	realtime := &channelHandlerTestRealtime{}
	unread := &fakeChannelUnreadReader{}
	handler := &ChannelMembershipHandler{channels: store, appStore: store, realtime: realtime, unreadRuntime: unread}

	_, err := handler.Leave(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, []string{"wsp_1:usr_1:chn_1"}, realtime.scopeUnsubscribed)
}

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

	store := &channelHandlerTestStore{
		memberEventResult: message.ChannelMemberEventResult{
			Changed:           true,
			AffectedMemberIDs: []string{"mem_changed"},
		},
	}
	realtime := &channelHandlerTestRealtime{}
	unread := &fakeChannelUnreadReader{}
	handler := &ChannelMembershipHandler{channels: store, appStore: store, realtime: realtime, unreadRuntime: unread}

	privateResponse, err := handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{AgentID: " agt_private "}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "agt_private", store.privateAgent.agentID)
	require.Equal(t, dto.EmptyResponse{}, privateResponse.Body)

	publicResponse, err := handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{AgentID: " agt_public ", Visibility: " public "}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "agt_public", store.addedAgent.agentID)
	require.Equal(t, dto.EmptyResponse{}, publicResponse.Body)

	serviceResponse, err := handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{ServiceMemberID: " svc_1 "}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "svc_1", store.addedService.serviceMemberID)
	require.Equal(t, dto.EmptyResponse{}, serviceResponse.Body)

	humanResponse, err := handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{UserID: " usr_2 "}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "usr_2", store.addedHuman.humanUserID)
	require.Equal(t, dto.EmptyResponse{}, humanResponse.Body)
	require.Len(t, realtime.membersUpdated, 4)
	require.Len(t, realtime.updated, 4)
	require.Equal(t, []unreadservice.ScopeInvalidated{
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_add"},
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_add"},
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_add"},
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_add"},
	}, unread.invalidated)
	require.Empty(t, realtime.unreadUpdated)

	_, err = handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{AgentID: "agt_1", UserID: "usr_2"}, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "agentId, userId, and serviceMemberId are mutually exclusive")

	_, err = handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{AgentID: "agt_1", Visibility: "invalid"}, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "agent visibility must be public or private")
}

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

	store := &channelHandlerTestStore{
		memberEventResult: message.ChannelMemberEventResult{Changed: true},
	}
	unread := &fakeChannelUnreadReader{}
	handler := &ChannelMembershipHandler{channels: store, appStore: store, unreadRuntime: unread}

	_, err := handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{UserID: " usr_2 "}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Empty(t, unread.invalidated)
}

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

	store := &channelHandlerTestStore{
		memberEventResult: message.ChannelMemberEventResult{
			Changed:           true,
			AffectedMemberIDs: []string{"mem_changed"},
			Message:           message.AppMessage{ID: "msg_member_event", ChannelID: "chn_1", MessageType: message.MessageTypeSystem},
			Delivery:          message.MessageDelivery{MessageID: "msg_member_event"},
		},
	}
	realtime := &channelHandlerTestRealtime{}
	unread := &fakeChannelUnreadReader{}
	fanout := &channelHandlerTestFanout{}
	handler := &ChannelMembershipHandler{appStore: store, realtime: realtime, unreadRuntime: unread, messageFanout: fanout}

	response, err := handler.UpdateAgentVisibility(channelWorkspaceJSONWithParams(dto.UpdateChannelAgentVisibilityRequest{Visibility: " Public "}, gin.Param{Key: "channelId", Value: "chn_1"}, gin.Param{Key: "agentId", Value: " agt_1 "}))
	require.NoError(t, err)
	require.Equal(t, dto.EmptyResponse{}, response.Body)
	require.Equal(t, "usr_1", store.updatedAgentVisibility.userID)
	require.Equal(t, "wsp_1", store.updatedAgentVisibility.workspaceID)
	require.Equal(t, "chn_1", store.updatedAgentVisibility.channelID)
	require.Equal(t, "agt_1", store.updatedAgentVisibility.agentID)
	require.Equal(t, "public", store.updatedAgentVisibility.visibility)
	require.Equal(t, []string{"wsp_1:chn_1"}, realtime.membersUpdated)
	require.Equal(t, []string{"wsp_1"}, realtime.updated)
	require.Equal(t, []unreadservice.ScopeInvalidated{{
		WorkspaceID: "wsp_1",
		ScopeID:     "chn_1",
		MemberIDs:   []string{"mem_changed"},
		Reason:      "channel_member_visibility",
	}}, unread.invalidated)
	require.Equal(t, "wsp_1:chn_1:msg_member_event", fanout.message)
	require.True(t, fanout.projectionOnly)

	_, err = handler.UpdateAgentVisibility(channelWorkspaceJSONWithParams(dto.UpdateChannelAgentVisibilityRequest{Visibility: "shared"}, gin.Param{Key: "channelId", Value: "chn_1"}, gin.Param{Key: "agentId", Value: "agt_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "agent visibility must be public or private")
}

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

	store := &channelHandlerTestStore{
		memberEventResult: message.ChannelMemberEventResult{
			Changed:  true,
			Message:  message.AppMessage{ID: "msg_member_event", ChannelID: "chn_1", MessageType: message.MessageTypeSystem},
			Delivery: message.MessageDelivery{MessageID: "msg_member_event"},
		},
	}
	realtime := &channelHandlerTestRealtime{}
	fanout := &channelHandlerTestFanout{}
	handler := &ChannelMembershipHandler{appStore: store, realtime: realtime, messageFanout: fanout}

	response, err := handler.BatchAddMembers(channelWorkspaceJSONWithParams(dto.BatchAddChannelMembersRequest{Members: []dto.AddChannelMemberRequest{
		{UserID: " usr_2 "},
		{AgentID: " agt_1 ", Visibility: " public "},
		{ServiceMemberID: " svc_1 "},
	}}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, []message.ChannelMemberAddRequest{
		{UserID: "usr_2"},
		{AgentID: "agt_1", Visibility: "public"},
		{ServiceMemberID: "svc_1"},
	}, store.batchAdded.members)
	require.Equal(t, dto.BatchAddChannelMembersResponse{
		AddedCount: 3,
		Members: []dto.AddChannelMemberRequest{
			{UserID: "usr_2"},
			{AgentID: "agt_1", Visibility: "public"},
			{ServiceMemberID: "svc_1"},
		},
	}, response.Body)
	require.Equal(t, []string{"wsp_1:chn_1"}, realtime.membersUpdated)
	require.Equal(t, []string{"wsp_1"}, realtime.updated)
	require.Equal(t, "wsp_1:chn_1:msg_member_event", fanout.message)
	require.True(t, fanout.projectionOnly)

	_, err = handler.BatchAddMembers(channelWorkspaceJSONWithParams(dto.BatchAddChannelMembersRequest{}, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "members is required")

	_, err = handler.BatchAddMembers(channelWorkspaceJSONWithParams(dto.BatchAddChannelMembersRequest{Members: []dto.AddChannelMemberRequest{{UserID: "usr_2", Visibility: "public"}}}, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "visibility is only supported for agent members")
}

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

	store := &channelHandlerTestStore{
		memberEventResult: message.ChannelMemberEventResult{
			Changed:  true,
			Message:  message.AppMessage{ID: "msg_member_event", ChannelID: "chn_1", MessageType: message.MessageTypeSystem},
			Delivery: message.MessageDelivery{MessageID: "msg_member_event"},
		},
	}
	fanout := &channelHandlerTestFanout{}
	handler := &ChannelMembershipHandler{appStore: store, messageFanout: fanout}

	_, err := handler.AddMember(channelWorkspaceJSONWithParams(dto.AddChannelMemberRequest{UserID: " usr_2 "}, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "wsp_1:chn_1:msg_member_event", fanout.message)
	require.True(t, fanout.projectionOnly)
}

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

	store := &channelHandlerTestStore{
		listRooms: []string{"chn_1:root", "chn_1:thread"},
		memberEventResult: message.ChannelMemberEventResult{
			Changed:           true,
			AffectedMemberIDs: []string{"mem_changed"},
		},
	}
	realtime := &channelHandlerTestRealtime{}
	unread := &fakeChannelUnreadReader{}
	handler := &ChannelMembershipHandler{appStore: store, realtime: realtime, unreadRuntime: unread}

	agentResponse, err := handler.RemoveAgentMember(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}, gin.Param{Key: "agentId", Value: "agt_1"}))
	require.NoError(t, err)
	require.Equal(t, "agt_1", store.removedAgent.agentID)
	require.Equal(t, dto.EmptyResponse{}, agentResponse.Body)
	require.Equal(t, []string{"wsp_1:chn_1:agt_1"}, realtime.wakeCleared)

	serviceResponse, err := handler.RemoveServiceMember(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}, gin.Param{Key: "memberId", Value: "svc_1"}))
	require.NoError(t, err)
	require.Equal(t, "svc_1", store.removedService.serviceMemberID)
	require.Equal(t, dto.EmptyResponse{}, serviceResponse.Body)

	userResponse, err := handler.RemoveUserMember(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}, gin.Param{Key: "userId", Value: "usr_2"}))
	require.NoError(t, err)
	require.Equal(t, "usr_2", store.removedHuman.humanUserID)
	require.Equal(t, dto.EmptyResponse{}, userResponse.Body)
	require.Equal(t, []string{"wsp_1:usr_2:chn_1", "wsp_1:usr_2:chn_1:root", "wsp_1:usr_2:chn_1:thread"}, realtime.scopeUnsubscribed)
	require.Len(t, realtime.membersUpdated, 3)
	require.Len(t, realtime.updated, 3)
	require.Equal(t, []unreadservice.ScopeInvalidated{
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_remove"},
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_remove"},
		{WorkspaceID: "wsp_1", ScopeID: "chn_1", MemberIDs: []string{"mem_changed"}, Reason: "channel_member_remove"},
	}, unread.invalidated)
}
