package channel

import (
	"bytes"
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// --- Fake stores ---

type fakeThreadStore struct {
	listResult           []message.SubscribedThread
	listErr              error
	listPageResult       message.ThreadInboxPage
	listPageErr          error
	getResult            message.SubscribedThread
	getErr               error
	membershipResult     message.ThreadMembership
	membershipErr        error
	countResult          message.ThreadInboxCount
	countErr             error
	subscribeErr         error
	setSubscribeCalled   bool
	subscribeCalls       int
	subscribedThreadID   string
	unsubscribeErr       error
	listLimit            int
	listCursor           message.ThreadInboxCursor
	listForChannelResult map[string]message.ReplyThreadSummary
	listForChannelErr    error
	listForChannelLimit  int
	openResult           message.ReplyThreadSummary
	openErr              error
	conversationResult   message.ConversationItem
	conversationErr      error
	conversationScopeID  string
	conversationHidden   bool
	subsystemChannel     message.ChannelSummary
	subsystemChannelErr  error
}

func (f *fakeThreadStore) EnsureAgentThreadSessionChannelForUser(_ context.Context, _ string, _ string) (message.ChannelSummary, error) {
	if f.subsystemChannelErr != nil {
		return message.ChannelSummary{}, f.subsystemChannelErr
	}
	return f.subsystemChannel, nil
}

func (f *fakeThreadStore) ListSubscribedThreadsForUserWithLimit(_ context.Context, _ string, _ string, limit int) ([]message.SubscribedThread, error) {
	f.listLimit = limit
	if f.listErr != nil {
		return nil, f.listErr
	}
	return f.listResult, nil
}

func (f *fakeThreadStore) ListSubscribedThreadInboxPageForUser(_ context.Context, _ string, _ string, limit int, cursor message.ThreadInboxCursor) (message.ThreadInboxPage, error) {
	f.listLimit = limit
	f.listCursor = cursor
	if f.listPageErr != nil {
		return message.ThreadInboxPage{}, f.listPageErr
	}
	return f.listPageResult, nil
}

func (f *fakeThreadStore) CountSubscribedThreadsForUser(_ context.Context, _ string, _ string) (message.ThreadInboxCount, error) {
	if f.countErr != nil {
		return message.ThreadInboxCount{}, f.countErr
	}
	return f.countResult, nil
}

func (f *fakeThreadStore) GetReplyThreadForUser(_ context.Context, _ string, _ string, _ string) (message.SubscribedThread, error) {
	if f.getErr != nil {
		return message.SubscribedThread{}, f.getErr
	}
	return f.getResult, nil
}

func (f *fakeThreadStore) GetThreadMembershipForUser(_ context.Context, _ string, _ string, _ string) (message.ThreadMembership, error) {
	if f.membershipErr != nil {
		return message.ThreadMembership{}, f.membershipErr
	}
	return f.membershipResult, nil
}

func (f *fakeThreadStore) LoadConversationForUser(_ context.Context, _ string, _ string, scopeID string, includeHidden bool) (message.ConversationItem, error) {
	f.conversationScopeID = scopeID
	f.conversationHidden = includeHidden
	if f.conversationErr != nil {
		return message.ConversationItem{}, f.conversationErr
	}
	return f.conversationResult, nil
}

func (f *fakeThreadStore) SubscribeThreadForUser(_ context.Context, _ string, _ string, threadChannelID string) error {
	f.subscribeCalls++
	f.subscribedThreadID = threadChannelID
	return f.subscribeErr
}

func (f *fakeThreadStore) SetThreadSubscriptionStateForUser(_ context.Context, _ string, _ string, _ string, _ string) error {
	f.setSubscribeCalled = true
	return f.subscribeErr
}

func (f *fakeThreadStore) UnsubscribeThreadForUser(_ context.Context, _ string, _ string, _ string) error {
	return f.unsubscribeErr
}

func (f *fakeThreadStore) ListReplyThreadSummariesForUserWithLimit(_ context.Context, _ string, _ string, _ string, limit int) (map[string]message.ReplyThreadSummary, error) {
	f.listForChannelLimit = limit
	if f.listForChannelErr != nil {
		return nil, f.listForChannelErr
	}
	return f.listForChannelResult, nil
}

func (f *fakeThreadStore) OpenReplyThreadForUser(_ context.Context, _ string, _ string, _ string, _ string) (message.ReplyThreadSummary, error) {
	if f.openErr != nil {
		return message.ReplyThreadSummary{}, f.openErr
	}
	return f.openResult, nil
}

// --- ThreadHandler tests ---

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/threads", handleChannelWorkspace(handler.List))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusUnauthorized, recorder.Code)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		handleChannelWorkspace(handler.List)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "No workspace selected")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		handleChannelWorkspace(handler.ListForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/channels/chn_1/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "No workspace selected")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/channels/%20/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "channelId is required")
}

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

	store := &fakeThreadStore{
		listForChannelResult: map[string]message.ReplyThreadSummary{
			"msg_1": {
				ThreadChannelID:   "thr_1",
				SubscriptionState: message.HumanScopeSubscriptionStateQuiet,
			},
		},
	}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.GET("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/channels/chn_1/threads?limit=30", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 30, store.listForChannelLimit)
	require.Contains(t, recorder.Body.String(), `"subscriptionState":"quiet"`)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.POST("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		handleChannelWorkspaceJSON(handler.OpenForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/channels/chn_1/threads", bytes.NewBufferString(`{"parentMessageId":"msg_1"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "No workspace selected")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.POST("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspaceJSON(handler.OpenForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/channels/chn_1/threads", bytes.NewBufferString(`invalid json`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Invalid request body")
}

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

	store := &fakeThreadStore{
		openResult: message.ReplyThreadSummary{
			ThreadChannelID: "thr_1",
			ReplyCount:      0,
			ParticipantIDs:  []string{},
		},
		membershipResult: message.ThreadMembership{
			ThreadChannelID:   "thr_1",
			Subscribed:        true,
			SubscriptionState: "active",
		},
		conversationResult: message.ConversationItem{
			Kind:          message.ConversationKindThread,
			ScopeID:       "thr_1",
			ParentScopeID: "chn_1",
			Title:         "Thread",
		},
	}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.POST("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspaceJSON(handler.OpenForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/channels/chn_1/threads", bytes.NewBufferString(`{"parentMessageId":"msg_1"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Zero(t, store.subscribeCalls)
	require.Empty(t, store.conversationScopeID)
	require.False(t, store.conversationHidden)
	require.False(t, store.setSubscribeCalled)
	require.Contains(t, recorder.Body.String(), `"threadChannelId":"thr_1"`)
	require.NotContains(t, recorder.Body.String(), `"conversation"`)
}

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

	store := &fakeThreadStore{
		openResult: message.ReplyThreadSummary{
			ThreadChannelID: "thr_1",
			ReplyCount:      2,
			ParticipantIDs:  []string{"mem_peer"},
		},
		membershipResult: message.ThreadMembership{
			ThreadChannelID: "thr_1",
			Subscribed:      false,
		},
		subscribeErr: errors.New("open should not subscribe"),
	}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.POST("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspaceJSON(handler.OpenForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/channels/chn_1/threads", bytes.NewBufferString(`{"parentMessageId":"msg_1"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Zero(t, store.subscribeCalls)
	require.False(t, store.setSubscribeCalled)
	require.Empty(t, store.conversationScopeID)
	require.Contains(t, recorder.Body.String(), `"threadChannelId":"thr_1"`)
	require.NotContains(t, recorder.Body.String(), `"conversation"`)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.POST("/api/threads/:threadChannelId/subscribe", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		handleChannelWorkspace(handler.Subscribe)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/threads/chn_1/subscribe", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "No workspace selected")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.POST("/api/threads/:threadChannelId/unsubscribe", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		handleChannelWorkspace(handler.Unsubscribe)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/threads/chn_1/unsubscribe", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "No workspace selected")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.POST("/api/threads/:threadChannelId/subscribe", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.Subscribe)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/threads/%20/subscribe", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "threadChannelId is required")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.List)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
}

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

	store := &fakeThreadStore{}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.GET("/api/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.List)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, defaultThreadListLimit, store.listLimit)
}

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

	store := &fakeThreadStore{}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.GET("/api/channels/:channelId/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListForChannel)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/channels/chn_1/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, defaultThreadListLimit, store.listForChannelLimit)
}

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

	store := &fakeThreadStore{}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.GET("/api/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.List)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads?limit=5", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 5, store.listLimit)
}

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

	store := &fakeThreadStore{}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.GET("/api/v1/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListV1)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads?limit=1000", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, maxThreadListLimit, store.listLimit)
}

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

	store := &fakeThreadStore{
		subsystemChannel: message.ChannelSummary{ChannelID: "chn_kb"},
		listForChannelResult: map[string]message.ReplyThreadSummary{
			"msg_1": {
				ThreadChannelID:      "thr_1",
				RootMessageID:        "msg_1",
				ParentMessagePreview: "@Agent Explain",
				ReplyCount:           2,
				ParticipantIDs:       []string{},
			},
		},
	}
	handler := &ThreadHandler{store: store, subsystemChannels: store}
	router := gin.New()
	router.GET("/api/v1/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListV1)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads?subsystem=kb&limit=5", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 5, store.listForChannelLimit)
	require.Contains(t, recorder.Body.String(), "thr_1")
}

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

	store := &fakeThreadStore{subsystemChannel: message.ChannelSummary{ChannelID: "chn_kb"}}
	handler := &ThreadHandler{store: store, subsystemChannels: store}
	router := gin.New()
	router.GET("/api/v1/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListV1)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads?subsystem=unknown&limit=5", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "unsupported thread subsystem")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/v1/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListV1)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "limit is required")
}

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

	store := &fakeThreadStore{
		listPageResult: message.ThreadInboxPage{
			Threads: []message.SubscribedThread{
				{ThreadChannelID: "thr_1", ParentChannelID: "chn_1", ParentChannelType: "channel", ParentLabel: "agentteam", ParentMessageID: "msg_1", ParentMessageShortID: "00000001", ParticipantIDs: []string{}, ReplyCount: 1},
			},
			HasMore:    true,
			NextCursor: message.ThreadInboxCursor{SortAt: time.Date(2026, 5, 14, 4, 31, 0, 0, time.UTC), ThreadChannelID: "thr_2"},
		},
	}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.GET("/api/v1/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.ListV1)(ctx)
	})

	recorder := httptest.NewRecorder()
	inputCursor := message.ThreadInboxCursor{SortAt: time.Date(2026, 5, 14, 4, 30, 0, 0, time.UTC), ThreadChannelID: "thr_1"}
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads?limit=20&cursor="+converter.ToThreadInboxCursorToken(inputCursor), nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 20, store.listLimit)
	require.Equal(t, inputCursor, store.listCursor)
	require.Contains(t, recorder.Body.String(), `"nextCursor":"`+converter.ToThreadInboxCursorToken(store.listPageResult.NextCursor)+`"`)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{
		countResult: message.ThreadInboxCount{Count: 17},
	}}
	router := gin.New()
	router.GET("/api/v1/threads/count", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.Count)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads/count", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Contains(t, recorder.Body.String(), `"count":17`)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{
		membershipResult: message.ThreadMembership{
			ThreadChannelID:   "thr_1",
			Subscribed:        true,
			SubscriptionState: "quiet",
		},
	}}
	router := gin.New()
	router.GET("/api/v1/threads/:threadChannelId/membership", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.Membership)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/threads/thr_1/membership", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Contains(t, recorder.Body.String(), `"subscribed":true`)
	require.Contains(t, recorder.Body.String(), `"subscriptionState":"quiet"`)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.GET("/api/threads", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.List)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads?limit=-1", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "limit must be a positive integer")
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{
		getResult: message.SubscribedThread{
			ThreadChannelID:      "chn_thread",
			ParentChannelID:      "chn_parent",
			ParentChannelType:    "channel",
			ParentLabel:          "all",
			ParentMessageID:      "msg_parent",
			ParentMessageShortID: "msg_pare",
			ParticipantIDs:       []string{"mem_1"},
			Participants: []message.ReplyThreadParticipant{{
				ID:            "agt_1",
				Type:          "agent",
				DisplayName:   "Agent One",
				MemberID:      "mem_1",
				OwnerMemberID: "mem_owner",
				OwnerUserID:   "usr_owner",
				Visibility:    "private",
			}},
			ParticipantCount: 1,
			ReplyCount:       1,
		},
	}}
	router := gin.New()
	router.GET("/api/threads/:threadChannelId", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.Get)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/threads/chn_thread", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Contains(t, recorder.Body.String(), `"threadChannelId":"chn_thread"`)
	require.Contains(t, recorder.Body.String(), `"participantCount":1`)
	require.Contains(t, recorder.Body.String(), `"ownerUserId":"usr_owner"`)
	require.Contains(t, recorder.Body.String(), `"visibility":"private"`)
}

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

	store := &fakeThreadStore{
		conversationResult: message.ConversationItem{
			Kind:    message.ConversationKindThread,
			ScopeID: "chn_thread",
			Title:   "Thread",
		},
	}
	handler := &ThreadHandler{store: store}
	router := gin.New()
	router.POST("/api/threads/:threadChannelId/subscribe", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.Subscribe)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/threads/chn_thread/subscribe", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "chn_thread", store.conversationScopeID)
	require.True(t, store.conversationHidden)
	require.Contains(t, recorder.Body.String(), `"conversation":{"id":"thread:chn_thread"`)
}

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

	handler := &ThreadHandler{store: &fakeThreadStore{}}
	router := gin.New()
	router.POST("/api/threads/:threadChannelId/unsubscribe", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
		api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1"})
		handleChannelWorkspace(handler.Unsubscribe)(ctx)
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/threads/chn_thread/unsubscribe", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
}
