package im

import (
	"context"
	"testing"
	"time"

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

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

func TestPinsHandlerPinPublishesScopeEventWhenChanged(t *testing.T) {
	gin.SetMode(gin.TestMode)
	streamWriter := &recordingStreamWriter{}
	store := &fakePinStore{
		pinResult: message.MessagePinMutationResult{
			Changed:   true,
			MessageID: "msg_1",
			PinID:     "pin_1",
			Pin:       pinForHandlerTest("pin_1", "msg_1", false),
			List:      pinListForHandlerTest(false),
		},
	}
	handler := &PinsHandler{pins: store, streamWriter: streamWriter}

	response, err := handler.Pin(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"}, gin.Param{Key: "messageId", Value: "msg_1"}))

	require.NoError(t, err)
	body := response.Body.(dto.MessagePinMutationResponse)
	require.True(t, body.Changed)
	require.NotNil(t, body.Pin)
	require.Equal(t, "pin_1", body.Pin.ID)
	require.Len(t, streamWriter.scopeRequests, 1)
	require.Empty(t, streamWriter.memberRequests)
	event := streamWriter.scopeRequests[0].Event
	require.Equal(t, imEventTypeMessagePinChanged, event.Type)
	require.Equal(t, "msg_1", event.EntityID)
	payload := event.Payload
	require.Equal(t, "chn_1", payload["scopeId"])
	require.Equal(t, "msg_1", payload["messageId"])
	require.Equal(t, true, payload["pinned"])
	require.Equal(t, "pins:1", payload["pinRevision"])
	require.Equal(t, "chn_1", store.pinScopeID)
	require.Equal(t, "msg_1", store.pinMessageID)
}

func TestPinsHandlerPinRejectsMismatchedReturnedListScope(t *testing.T) {
	gin.SetMode(gin.TestMode)
	streamWriter := &recordingStreamWriter{}
	store := &fakePinStore{
		pinResult: message.MessagePinMutationResult{
			Changed:   true,
			MessageID: "msg_thread_1",
			PinID:     "pin_1",
			Pin:       pinForHandlerTest("pin_1", "msg_thread_1", false),
			List:      pinListForHandlerTest(false),
		},
	}
	handler := &PinsHandler{pins: store, streamWriter: streamWriter}

	_, err := handler.Pin(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "thr_1"}, gin.Param{Key: "messageId", Value: "msg_thread_1"}))

	require.EqualError(t, err, "Internal server error")
	require.Empty(t, streamWriter.scopeRequests)
	require.Equal(t, "thr_1", store.pinScopeID)
}

func TestPinsHandlerListRejectsMismatchedReturnedListScope(t *testing.T) {
	gin.SetMode(gin.TestMode)
	handler := &PinsHandler{pins: &fakePinStore{list: pinListForHandlerTest(false)}}

	_, err := handler.List(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "thr_1"}))

	require.EqualError(t, err, "Internal server error")
}

func TestPinsHandlerListRejectsPinFromAnotherScope(t *testing.T) {
	gin.SetMode(gin.TestMode)
	list := pinListForHandlerTest(false)
	list.Pins[0].ScopeID = "thr_1"
	handler := &PinsHandler{pins: &fakePinStore{list: list}}

	_, err := handler.List(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"}))

	require.EqualError(t, err, "Internal server error")
}

func TestPinsHandlerSharedNoopDoesNotPublishScopeEvent(t *testing.T) {
	gin.SetMode(gin.TestMode)
	streamWriter := &recordingStreamWriter{}
	handler := &PinsHandler{
		pins: &fakePinStore{pinResult: message.MessagePinMutationResult{
			Changed:   false,
			MessageID: "msg_1",
			PinID:     "pin_1",
			Pin:       pinForHandlerTest("pin_1", "msg_1", false),
			List:      pinListForHandlerTest(false),
		}},
		streamWriter: streamWriter,
	}

	response, err := handler.Pin(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"}, gin.Param{Key: "messageId", Value: "msg_1"}))

	require.NoError(t, err)
	body := response.Body.(dto.MessagePinMutationResponse)
	require.False(t, body.Changed)
	require.Empty(t, streamWriter.scopeRequests)
	require.Empty(t, streamWriter.memberRequests)
}

func TestPinsHandlerTopPublishesScopeEventWithoutViewerLocalState(t *testing.T) {
	gin.SetMode(gin.TestMode)
	streamWriter := &recordingStreamWriter{}
	store := &fakePinStore{topResult: message.MessageTopNoticeMutationResult{
		Changed:     true,
		MessageID:   "msg_1",
		TopNoticeID: "top_1",
		TopNotice:   topNoticeForHandlerTest("top_1", "msg_1", false),
		State:       topNoticeStateForHandlerTest(false),
	}}
	handler := &PinsHandler{pins: store, streamWriter: streamWriter}

	response, err := handler.Top(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"}, gin.Param{Key: "messageId", Value: "msg_1"}))

	require.NoError(t, err)
	body := response.Body.(dto.MessageTopNoticeMutationResponse)
	require.True(t, body.Changed)
	require.NotNil(t, body.TopNotice)
	require.Len(t, streamWriter.scopeRequests, 1)
	require.Empty(t, streamWriter.memberRequests)
	event := streamWriter.scopeRequests[0].Event
	require.Equal(t, imEventTypeMessageTopNoticeChanged, event.Type)
	require.Equal(t, "top_1", event.EntityID)
	payload := event.Payload
	require.Equal(t, "chn_1", payload["scopeId"])
	require.Equal(t, "msg_1", payload["messageId"])
	require.Equal(t, "top_1", payload["topNoticeId"])
	require.Equal(t, true, payload["hasTopNotice"])
	require.Equal(t, "top:1", payload["topNoticeRevision"])
	require.NotContains(t, payload, "state")
	require.NotContains(t, payload, "topNotice")
}

func TestPinsHandlerHidePublishesMemberScopedTopBannerEvent(t *testing.T) {
	gin.SetMode(gin.TestMode)
	streamWriter := &recordingStreamWriter{}
	store := &fakePinStore{topResult: message.MessageTopNoticeMutationResult{
		Changed:     true,
		MessageID:   "msg_1",
		TopNoticeID: "top_1",
		State:       topNoticeStateForHandlerTest(true),
	}}
	handler := &PinsHandler{pins: store, streamWriter: streamWriter}

	response, err := handler.HideTopBannerForMe(imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"}, gin.Param{Key: "messageId", Value: "msg_1"}))

	require.NoError(t, err)
	body := response.Body.(dto.MessageTopNoticeMutationResponse)
	require.True(t, body.Changed)
	require.Empty(t, streamWriter.scopeRequests)
	require.Len(t, streamWriter.memberRequests, 1)
	recorded := streamWriter.memberRequests[0]
	require.Equal(t, "mem_1", recorded.memberID)
	require.Equal(t, "chn_1", recorded.scopeID)
	require.Equal(t, imEventTypeMessageTopNoticeBannerState, recorded.event.Type)
	payload := recorded.event.Payload
	require.Equal(t, "chn_1", payload["scopeId"])
	require.Equal(t, "msg_1", payload["messageId"])
	require.Equal(t, "top_1", payload["topNoticeId"])
	require.Equal(t, true, payload["hiddenFromBannerForMe"])
	require.Equal(t, "top_viewer:1", payload["viewerTopNoticeRevision"])
}

func TestPinsHandlerListParsesOpaqueCursor(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cursorTime := time.Date(2026, 6, 16, 8, 0, 0, 0, time.UTC)
	store := &fakePinStore{list: pinListForHandlerTest(false)}
	request := imWorkspaceRequest(gin.Param{Key: "scopeId", Value: "chn_1"})
	request.Gin.Request.URL.RawQuery = "cursor=" + cursorTime.Format(time.RFC3339Nano) + "|pin_1&limit=10"
	handler := &PinsHandler{pins: store}

	response, err := handler.List(request)

	require.NoError(t, err)
	body := response.Body.(dto.MessagePinListResponse)
	require.Equal(t, "chn_1", body.ScopeID)
	require.Equal(t, "pin_1", store.cursor.ID)
	require.Equal(t, cursorTime, store.cursor.PinnedAt)
	require.Equal(t, 10, store.limit)
}

type fakePinStore struct {
	list         message.MessagePinList
	pinResult    message.MessagePinMutationResult
	unpinResult  message.MessagePinMutationResult
	topState     message.MessageTopNoticeState
	topResult    message.MessageTopNoticeMutationResult
	cursor       message.MessagePinCursor
	limit        int
	pinScopeID   string
	pinMessageID string
}

func (store *fakePinStore) ListMessagePinsForUser(_ context.Context, _ string, _ string, scopeID string, cursor message.MessagePinCursor, limit int) (message.MessagePinList, error) {
	store.cursor = cursor
	store.limit = limit
	if store.list.ScopeID == "" {
		store.list.ScopeID = scopeID
	}
	return store.list, nil
}

func (store *fakePinStore) PinMessageForUser(_ context.Context, _ string, _ string, scopeID string, messageID string) (message.MessagePinMutationResult, error) {
	store.pinScopeID = scopeID
	store.pinMessageID = messageID
	return store.pinResult, nil
}

func (store *fakePinStore) UnpinMessageForEveryoneForUser(context.Context, string, string, string, string) (message.MessagePinMutationResult, error) {
	return store.unpinResult, nil
}

func (store *fakePinStore) GetMessageTopNoticeForUser(context.Context, string, string, string) (message.MessageTopNoticeState, error) {
	return store.topState, nil
}

func (store *fakePinStore) TopMessageForUser(context.Context, string, string, string, string) (message.MessageTopNoticeMutationResult, error) {
	return store.topResult, nil
}

func (store *fakePinStore) UntopMessageForEveryoneForUser(context.Context, string, string, string, string) (message.MessageTopNoticeMutationResult, error) {
	return store.topResult, nil
}

func (store *fakePinStore) HideMessageTopNoticeBannerForUser(context.Context, string, string, string, string, string) (message.MessageTopNoticeMutationResult, error) {
	return store.topResult, nil
}

func (store *fakePinStore) ShowMessageTopNoticeBannerForUser(context.Context, string, string, string, string, string) (message.MessageTopNoticeMutationResult, error) {
	return store.topResult, nil
}

func (store *fakePinStore) ResetMessageTopNoticeHidesForUser(context.Context, string, string, string) (message.MessageTopNoticeMutationResult, error) {
	return store.topResult, nil
}

func pinListForHandlerTest(hidden bool) message.MessagePinList {
	return message.MessagePinList{
		ScopeID:                "chn_1",
		ViewerMemberID:         "mem_1",
		Pins:                   []message.MessagePin{*pinForHandlerTest("pin_1", "msg_1", hidden)},
		LatestPin:              pinForHandlerTest("pin_1", "msg_1", hidden),
		PinRevision:            "pins:1",
		ViewerPinStateRevision: "pin_viewer:1",
	}
}

func topNoticeStateForHandlerTest(hidden bool) message.MessageTopNoticeState {
	return message.MessageTopNoticeState{
		ScopeID:                 "chn_1",
		ViewerMemberID:          "mem_1",
		TopNotice:               topNoticeForHandlerTest("top_1", "msg_1", hidden),
		TopNoticeRevision:       "top:1",
		ViewerTopNoticeRevision: "top_viewer:1",
	}
}

func pinForHandlerTest(pinID string, messageID string, _ bool) *message.MessagePin {
	pinnedAt := time.Date(2026, 6, 16, 8, 0, 0, 0, time.UTC)
	return &message.MessagePin{
		ID:               pinID,
		WorkspaceID:      "wsp_1",
		ScopeID:          "chn_1",
		MessageID:        messageID,
		PinnedByMemberID: "mem_1",
		PinnedAt:         pinnedAt,
		Message: message.AppMessage{
			ID:        messageID,
			Seq:       7,
			ChannelID: "chn_1",
			Content:   "hello",
			CreatedAt: pinnedAt,
			PinState: &message.MessagePinState{
				IsPinned:         true,
				PinID:            pinID,
				PinnedAt:         pinnedAt,
				PinnedByMemberID: "mem_1",
			},
		},
		ViewerState: message.MessagePinViewerState{},
		ViewerCapabilities: message.MessagePinViewerCapabilities{
			CanJump:             true,
			CanUnpinForEveryone: true,
		},
	}
}

func topNoticeForHandlerTest(topNoticeID string, messageID string, hidden bool) *message.MessageTopNotice {
	setAt := time.Date(2026, 6, 16, 8, 0, 0, 0, time.UTC)
	return &message.MessageTopNotice{
		ID:            topNoticeID,
		WorkspaceID:   "wsp_1",
		ScopeID:       "chn_1",
		MessageID:     messageID,
		SetByMemberID: "mem_1",
		SetAt:         setAt,
		Message: message.AppMessage{
			ID:        messageID,
			Seq:       7,
			ChannelID: "chn_1",
			Content:   "hello",
			CreatedAt: setAt,
		},
		ViewerState: message.MessageTopNoticeViewerState{HiddenFromBannerForMe: hidden},
		ViewerCapabilities: message.MessageTopNoticeViewerCapabilities{
			CanJump:                true,
			CanRemoveForEveryone:   true,
			CanHideFromBannerForMe: !hidden,
			CanShowInBannerAgain:   hidden,
		},
	}
}
