package im

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

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

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

func TestSnapshotIncludesFirstPaintWorkspaceData(t *testing.T) {
	gin.SetMode(gin.TestMode)
	lastAt := time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC)
	pinnedAt := time.Date(2026, 6, 10, 11, 30, 0, 0, time.UTC)
	handler := &SnapshotHandler{
		channels: fakeSnapshotChannelStore{channels: []message.ChannelSummary{{
			ChannelID:   "chn_1",
			Slug:        "general",
			DisplayName: "General",
			Description: "General channel",
			Tutorial:    true,
			Joined:      true,
			PinnedAt:    &pinnedAt,
		}}},
		dms: fakeSnapshotDMStore{channels: []message.DMChannel{{
			ID:            "dm_1",
			PeerType:      "user",
			PeerID:        "usr_2",
			PeerName:      "Alice",
			SidebarHidden: false,
		}}},
		conversationIndex: &fakeConversationIndexStore{page: snapshotConversationIndexPage(lastAt)},
		conversationSidebarThreads: fakeSnapshotConversationSidebarThreadStore{page: message.ThreadInboxPage{Threads: []message.SubscribedThread{{
			ThreadChannelID:      "thr_1",
			ParentChannelID:      "chn_1",
			ParentChannelType:    "channel",
			ParentLabel:          "General",
			ParentMessageID:      "msg_parent_1",
			ParentMessageShortID: "msg_parent_",
			ParentMessagePreview: "Root message title",
			Title:                "Root message title",
			ParticipantIDs:       []string{},
			ReplyCount:           4,
		}}}},
		streamEvents: &fakeWSStreamStore{events: []storeim.StreamEvent{{
			WorkspaceID: "wsp_1",
			MemberID:    "mem_1",
			EventSeq:    7,
		}}},
		messageSearch: fakeSnapshotSearchStore{enabled: true},
		unreadRuntime: &fakeUnreadRuntime{states: map[string]unread.ScopeUnreadState{
			"chn_1": {ScopeID: "chn_1", MessageUnreadCount: 3, ServingState: unread.ServingStateReady},
			"thr_1": {ScopeID: "thr_1", MessageUnreadCount: 2, ServingState: unread.ServingStateReady},
		}},
	}

	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/?clientId=web", nil)

	response, err := handler.Get(request)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body := response.Body.(snapshotResponse)
	require.Len(t, body.Channels, 1)
	require.Equal(t, "general", body.Channels[0].Slug)
	require.True(t, body.Channels[0].Tutorial)
	require.Equal(t, &pinnedAt, body.Channels[0].PinnedAt)
	require.Len(t, body.DMChannels, 1)
	require.Equal(t, "dm_1", body.DMChannels[0].ID)
	require.Equal(t, int64(7), body.SnapshotEventSeq)
	require.Equal(t, int64(7), body.StreamCursor.LatestEventSeq)
	require.True(t, body.MessageSearchEnabled)
	require.Equal(t, map[string]int64{"chn_1": 3, "thr_1": 2}, body.UnreadCounts)
	require.Equal(t, int64(2), body.UnreadConversationCount)
	require.Equal(t, int64(5), body.Unread.Total)
	require.Len(t, body.SubscribedThreads, 1)
	require.Equal(t, "thr_1", body.SubscribedThreads[0].ThreadChannelID)
	require.Equal(t, "chn_1", body.SubscribedThreads[0].ParentChannelID)
	require.Equal(t, "Root message title", body.SubscribedThreads[0].ParentMessagePreview)
	require.Equal(t, "Root message title", body.SubscribedThreads[0].Title)
}

func TestSnapshotPrefersMemberLocalConversationIndex(t *testing.T) {
	gin.SetMode(gin.TestMode)
	legacy := fakeConversationStore{
		page: message.ConversationPage{
			Items: []message.ConversationItem{{
				Kind:    message.ConversationKindChannel,
				ScopeID: "legacy_channel",
				Title:   "legacy",
			}},
		},
	}
	handler := &SnapshotHandler{
		conversations: &legacy,
		conversationIndex: &fakeConversationIndexStore{
			page: snapshotConversationIndexPage(time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC)),
		},
	}

	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/", nil)

	response, err := handler.Get(request)
	require.NoError(t, err)
	body := response.Body.(snapshotResponse)
	require.Len(t, body.Conversations.Items, 2)
	require.Equal(t, "channel:chn_1", body.Conversations.Items[0].ID)
	require.Equal(t, "General", body.Conversations.Items[0].Title)
}

func TestSnapshotAdvancesStreamCursorToSnapshotFence(t *testing.T) {
	gin.SetMode(gin.TestMode)
	streams := &fakeWSStreamStore{events: []storeim.StreamEvent{{
		WorkspaceID: "wsp_1",
		MemberID:    "mem_1",
		EventSeq:    7,
	}}}
	cursors := &fakeWSCursorStore{}
	handler := &SnapshotHandler{
		conversationIndex: &fakeConversationIndexStore{
			page: snapshotConversationIndexPage(time.Now().UTC()),
		},
		streamEvents:  streams,
		streamCursors: cursors,
	}
	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(
		http.MethodGet,
		"/?clientId=miniapp%3Ains_device_a%3Awsp_1&advanceStreamCursor=true",
		nil,
	)
	request.Gin.Request.Header.Set("X-AgentTeam-Route-Source", "mobile.miniapp")

	response, err := handler.Get(request)
	require.NoError(t, err)
	body := response.Body.(snapshotResponse)
	require.Equal(t, int64(7), body.SnapshotEventSeq)
	require.Equal(t, "miniapp:ins_device_a:wsp_1", body.StreamCursor.ClientID)
	require.Equal(t, int64(7), body.StreamCursor.AckedEventSeq)
	require.Equal(t, 1, cursors.updateCount())
	require.Equal(t, int64(7), cursors.cursor.AckedEventSeq)
}

func TestSnapshotAdvanceRejectsArbitraryOrMismatchedClient(t *testing.T) {
	gin.SetMode(gin.TestMode)
	for _, clientID := range []string{
		"web",
		"miniapp:wsp_1",
		"miniapp:ins_device_a:wsp_other",
		"miniapp:bad!device:wsp_1",
		"workspace-im:v1:wsp_1:usr_1:cid_0123456789abcdef0123456789abcdef",
	} {
		t.Run(clientID, func(t *testing.T) {
			cursors := &fakeWSCursorStore{}
			handler := &SnapshotHandler{
				streamEvents:  &fakeWSStreamStore{},
				streamCursors: cursors,
			}
			request := imWorkspaceRequest()
			request.Gin.Request = httptest.NewRequest(
				http.MethodGet,
				"/?clientId="+clientID+"&advanceStreamCursor=true",
				nil,
			)
			request.Gin.Request.Header.Set("X-AgentTeam-Route-Source", "mobile.miniapp")

			_, err := handler.Get(request)

			requireIMHTTPError(t, err, http.StatusBadRequest, "invalid snapshot clientId")
			require.Equal(t, 0, cursors.updateCount())
		})
	}
}

func TestSnapshotAdvanceAcceptsScopedWebClient(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cursors := &fakeWSCursorStore{}
	handler := &SnapshotHandler{
		conversationIndex: &fakeConversationIndexStore{
			page: snapshotConversationIndexPage(time.Now().UTC()),
		},
		streamEvents: &fakeWSStreamStore{events: []storeim.StreamEvent{{
			WorkspaceID: "wsp_1",
			MemberID:    "mem_1",
			EventSeq:    9,
		}}},
		streamCursors: cursors,
	}
	request := imWorkspaceRequest()
	clientID := "workspace-im:v1:wsp_1:usr_1:cid_0123456789abcdef0123456789abcdef"
	request.Gin.Request = httptest.NewRequest(
		http.MethodGet,
		"/?clientId="+clientID+"&advanceStreamCursor=true",
		nil,
	)

	response, err := handler.Get(request)

	require.NoError(t, err)
	body := response.Body.(snapshotResponse)
	require.Equal(t, int64(9), body.SnapshotEventSeq)
	require.Equal(t, clientID, body.StreamCursor.ClientID)
	require.Equal(t, int64(9), body.StreamCursor.AckedEventSeq)
	require.Equal(t, 1, cursors.updateCount())
}

func TestSnapshotAdvanceRejectsValidLookingClientFromUntrustedCaller(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cursors := &fakeWSCursorStore{}
	handler := &SnapshotHandler{
		streamEvents:  &fakeWSStreamStore{},
		streamCursors: cursors,
	}
	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(
		http.MethodGet,
		"/?clientId=miniapp%3Ains_device_a%3Awsp_1&advanceStreamCursor=true",
		nil,
	)

	_, err := handler.Get(request)

	requireIMHTTPError(t, err, http.StatusBadRequest, "invalid snapshot clientId")
	require.Equal(t, 0, cursors.updateCount())
}

func TestSnapshotAdvanceDoesNotMoveCursorWhenSnapshotAssemblyFails(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cursors := &fakeWSCursorStore{}
	handler := &SnapshotHandler{
		channels:      fakeSnapshotChannelStore{err: errors.New("channels unavailable")},
		streamEvents:  &fakeWSStreamStore{events: []storeim.StreamEvent{{WorkspaceID: "wsp_1", MemberID: "mem_1", EventSeq: 12}}},
		streamCursors: cursors,
	}
	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(
		http.MethodGet,
		"/?clientId=workspace-im:v1:wsp_1:usr_1:cid_0123456789abcdef0123456789abcdef&advanceStreamCursor=true",
		nil,
	)

	_, err := handler.Get(request)

	require.Error(t, err)
	require.Equal(t, 0, cursors.updateCount())
}

func TestSnapshotAdvanceKeepsInstallationCursorsIsolated(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cursors := &fakeWSCursorStore{}
	require.NoError(t, cursors.UpdateCursor(
		context.Background(),
		"wsp_1",
		"mem_1",
		"miniapp:ins_device_b:wsp_1",
		3,
	))
	cursors.updates = nil
	handler := &SnapshotHandler{
		conversationIndex: &fakeConversationIndexStore{
			page: snapshotConversationIndexPage(time.Now().UTC()),
		},
		streamEvents: &fakeWSStreamStore{events: []storeim.StreamEvent{{
			WorkspaceID: "wsp_1",
			MemberID:    "mem_1",
			EventSeq:    7,
		}}},
		streamCursors: cursors,
	}
	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(
		http.MethodGet,
		"/?clientId=miniapp%3Ains_device_a%3Awsp_1&advanceStreamCursor=true",
		nil,
	)
	request.Gin.Request.Header.Set("X-AgentTeam-Route-Source", "mobile.miniapp")

	_, err := handler.Get(request)

	require.NoError(t, err)
	deviceA, err := cursors.GetCursor(context.Background(), "wsp_1", "mem_1", "miniapp:ins_device_a:wsp_1")
	require.NoError(t, err)
	deviceB, err := cursors.GetCursor(context.Background(), "wsp_1", "mem_1", "miniapp:ins_device_b:wsp_1")
	require.NoError(t, err)
	require.Equal(t, int64(7), deviceA.AckedEventSeq)
	require.Equal(t, int64(3), deviceB.AckedEventSeq)
}

func TestSnapshotSupportsActiveScopeQueryAliases(t *testing.T) {
	gin.SetMode(gin.TestMode)
	messages := &fakeSnapshotMessageStore{}
	handler := &SnapshotHandler{messages: messages}
	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/?activeScopeId=chn_1&messageLimit=5", nil)

	response, err := handler.Get(request)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body := response.Body.(snapshotResponse)
	require.NotNil(t, body.ActiveMessages)
	require.Equal(t, "chn_1", body.ActiveMessages.ScopeID)
	require.True(t, body.ActiveMessages.Page.HasMore)
	require.Equal(t, int64(1), body.ActiveMessages.Page.NextCursor)
	require.Equal(t, "chn_1", messages.scopeID)
	require.Equal(t, 5, messages.limit)
}

func TestSnapshotUnreadConversationCountIncludesManualUnreadOnlyScopes(t *testing.T) {
	gin.SetMode(gin.TestMode)
	handler := &SnapshotHandler{
		conversationIndex: &fakeConversationIndexStore{page: snapshotConversationIndexPage(time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC))},
		streamEvents:      &fakeWSStreamStore{},
		unreadRuntime: &fakeUnreadRuntime{states: map[string]unread.ScopeUnreadState{
			"chn_1": {ScopeID: "chn_1", MessageUnreadCount: 3, ServingState: unread.ServingStateReady},
			"thr_1": {ScopeID: "thr_1", ManualUnread: true, ServingState: unread.ServingStateReady},
			"dm_1":  {ScopeID: "dm_1", ServingState: unread.ServingStateReady},
		}},
	}

	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/", nil)

	response, err := handler.Get(request)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body := response.Body.(snapshotResponse)

	require.Equal(t, map[string]int64{"chn_1": 3}, body.UnreadCounts)
	require.Equal(t, int64(2), body.UnreadConversationCount)
}

func TestSnapshotUnreadCountsIncludeTaskAttention(t *testing.T) {
	gin.SetMode(gin.TestMode)
	taskState := unread.ScopeUnreadState{
		ScopeID:            "custom_task_scope",
		ScopeType:          unread.ScopeTypeTask,
		MessageUnreadCount: 4,
		ServingState:       unread.ServingStateReady,
	}
	runtime := &fakeUnreadRuntime{states: map[string]unread.ScopeUnreadState{
		"chn_1":             {ScopeID: "chn_1", ScopeType: unread.ScopeTypeChannel, MessageUnreadCount: 3, ServingState: unread.ServingStateReady},
		"custom_task_scope": taskState,
	}}
	handler := &SnapshotHandler{
		conversationIndex: &fakeConversationIndexStore{page: snapshotConversationIndexPage(time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC))},
		streamEvents:      &fakeWSStreamStore{},
		unreadRuntime:     runtime,
	}

	request := imWorkspaceRequest()
	request.Gin.Request = httptest.NewRequest(http.MethodGet, "/", nil)
	response, err := handler.Get(request)
	require.NoError(t, err)
	body := response.Body.(snapshotResponse)

	require.Equal(t, map[string]int64{"chn_1": 3, "custom_task_scope": 4}, body.UnreadCounts)
	require.Equal(t, int64(2), body.UnreadConversationCount)
	require.Equal(t, int64(4), runtime.ScopeStates("wsp_1", "mem_1", []string{"custom_task_scope"})["custom_task_scope"].Count(),
		"Task detail must keep its per-scope unread count")
}

func snapshotConversationIndexPage(lastAt time.Time) storeim.ConversationIndexPage {
	return storeim.ConversationIndexPage{
		Items: []storeim.ConversationIndexItem{{
			ConversationID: "channel:chn_1",
			ScopeID:        "chn_1",
			Kind:           "channel",
			SortAt:         lastAt,
			Projection: map[string]any{
				"id":             "channel:chn_1",
				"kind":           "channel",
				"scopeId":        "chn_1",
				"route":          "/inbox/channel/chn_1",
				"title":          "General",
				"lastActivityAt": "2026-06-10T12:00:00Z",
				"lastMessageAt":  "2026-06-10T12:00:00Z",
				"unreadCount":    99,
			},
		}, {
			ConversationID: "thread:thr_1",
			ScopeID:        "thr_1",
			Kind:           "thread",
			SortAt:         lastAt.Add(-time.Minute),
			Projection: map[string]any{
				"id":                   "thread:thr_1",
				"kind":                 "replyThread",
				"scopeId":              "thr_1",
				"route":                "/inbox/channel/chn_1/thread/thr_1",
				"parentScopeId":        "chn_1",
				"parentScopeType":      "channel",
				"parentTitle":          "General",
				"parentMessageId":      "msg_parent_1",
				"parentMessageShortId": "msg_parent_",
				"parentMessageAuthor":  "Opener",
				"title":                "Root message title",
				"lastActivityAt":       "2026-06-10T11:59:00Z",
				"lastMessageAt":        "2026-06-10T11:59:00Z",
				"unreadCount":          88,
				"replyCount":           4,
			},
		}},
		HasMore: false,
	}
}

type fakeSnapshotChannelStore struct {
	channels []message.ChannelSummary
	err      error
}

type countingSnapshotChannelStore struct {
	calls int
}

func (store *countingSnapshotChannelStore) ListChannelsForUser(context.Context, string, string) ([]message.ChannelSummary, error) {
	store.calls++
	return nil, nil
}

func (store fakeSnapshotChannelStore) ListChannelsForUser(context.Context, string, string) ([]message.ChannelSummary, error) {
	if store.err != nil {
		return nil, store.err
	}
	return store.channels, nil
}

type fakeSnapshotDMStore struct {
	channels []message.DMChannel
}

type countingSnapshotDMStore struct {
	calls int
}

func (store *countingSnapshotDMStore) ListDMChannelsForUserWithLimit(context.Context, string, string, int) ([]message.DMChannel, error) {
	store.calls++
	return nil, nil
}

func (store *countingSnapshotDMStore) GetDMChannelForUser(context.Context, string, string, string) (message.DMChannel, error) {
	return message.DMChannel{}, nil
}

func (store *countingSnapshotDMStore) OpenDMChannelWithAgent(context.Context, string, string, string) (message.DMChannel, error) {
	return message.DMChannel{}, nil
}

func (store *countingSnapshotDMStore) OpenDMChannelWithUser(context.Context, string, string, string) (message.DMChannel, error) {
	return message.DMChannel{}, nil
}

func (store fakeSnapshotDMStore) ListDMChannelsForUserWithLimit(context.Context, string, string, int) ([]message.DMChannel, error) {
	return store.channels, nil
}

func (store fakeSnapshotDMStore) GetDMChannelForUser(context.Context, string, string, string) (message.DMChannel, error) {
	return message.DMChannel{}, nil
}

func (store fakeSnapshotDMStore) OpenDMChannelWithAgent(context.Context, string, string, string) (message.DMChannel, error) {
	return message.DMChannel{}, nil
}

func (store fakeSnapshotDMStore) OpenDMChannelWithUser(context.Context, string, string, string) (message.DMChannel, error) {
	return message.DMChannel{}, nil
}

type fakeSnapshotConversationSidebarThreadStore struct {
	page  message.ThreadInboxPage
	limit int
}

func (store fakeSnapshotConversationSidebarThreadStore) ListSubscribedThreadInboxPageForUser(context.Context, string, string, int, message.ThreadInboxCursor) (message.ThreadInboxPage, error) {
	return store.page, nil
}

type fakeSnapshotSearchStore struct {
	enabled bool
}

func (store fakeSnapshotSearchStore) SupportsWorkspaceMessageSearch(context.Context, string) bool {
	return store.enabled
}

type fakeSnapshotMessageStore struct {
	scopeID string
	limit   int
	page    *message.AppMessagePage
	err     error
}

func (store *fakeSnapshotMessageStore) ListMessagesAroundForUser(context.Context, string, string, string, string, int) (message.AppMessagePage, error) {
	return message.AppMessagePage{}, nil
}

func (store *fakeSnapshotMessageStore) ListMessagesForUser(_ context.Context, _ string, _ string, scopeID string, limit int, _ *int64, _ *int64) (message.AppMessagePage, error) {
	store.scopeID = scopeID
	store.limit = limit
	if store.err != nil {
		return message.AppMessagePage{}, store.err
	}
	if store.page != nil {
		return *store.page, nil
	}
	return message.AppMessagePage{
		Messages: []message.AppMessage{{
			ID:        "msg_1",
			ChannelID: scopeID,
			Content:   "hello",
			Seq:       1,
		}},
		HasMore:    true,
		NextCursor: 1,
	}, nil
}
