package im

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

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

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

type fakeIMHumanWriter struct{}

type recordingMessageQueryStore struct {
	scopeIDs []string
	options  []storemessage.MessageSearchOptions
}

func (*recordingMessageQueryStore) GetMessageDeliveryDiagnosticsForUser(context.Context, string, string, string) (storemessage.MessageDeliveryDiagnostics, error) {
	return storemessage.MessageDeliveryDiagnostics{}, nil
}

func (*recordingMessageQueryStore) SupportsMessageSearch(context.Context) bool {
	return true
}

func (store *recordingMessageQueryStore) SearchMessagesForUser(_ context.Context, _ string, _ string, _ string, scopeID string, options storemessage.MessageSearchOptions) (storemessage.MessageSearchResult, error) {
	store.scopeIDs = append(store.scopeIDs, scopeID)
	store.options = append(store.options, options)
	return storemessage.MessageSearchResult{}, nil
}

func TestMessagesSearchDefaultsToCursorPaginationAndKeepsExplicitOffset(t *testing.T) {
	testCases := []struct {
		name          string
		rawQuery      string
		wantUseCursor bool
		wantOffset    int
	}{
		{name: "first page", rawQuery: "q=needle&limit=10", wantUseCursor: true},
		{name: "legacy offset", rawQuery: "q=needle&limit=10&offset=10", wantOffset: 10},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			queries := &recordingMessageQueryStore{}
			handler := &MessagesHandler{queries: queries}
			request := imWorkspaceRequest()
			request.Gin.Request = httptest.NewRequest(http.MethodGet, "/?"+testCase.rawQuery, nil)

			response, err := handler.Search(request)

			require.NoError(t, err)
			require.Equal(t, http.StatusOK, response.Status)
			require.Len(t, queries.options, 1)
			require.Equal(t, testCase.wantUseCursor, queries.options[0].UseCursor)
			require.Equal(t, testCase.wantOffset, queries.options[0].Offset)
		})
	}
}

func TestMessagesSearchParsesAuthorFilter(t *testing.T) {
	testCases := []struct {
		name      string
		rawQuery  string
		want      storemessage.MessageAuthorFilter
		wantError string
	}{
		{
			name:     "member",
			rawQuery: "q=needle&scope_id=scp_1&author_kind=member&author_id=mem_1",
			want:     storemessage.MessageAuthorFilter{Kind: storemessage.MessageAuthorFilterMember, ID: "mem_1"},
		},
		{
			name:     "agent trims values",
			rawQuery: "q=needle&author_kind=%20agent%20&author_id=%20agt_1%20",
			want:     storemessage.MessageAuthorFilter{Kind: storemessage.MessageAuthorFilterAgent, ID: "agt_1"},
		},
		{
			name:     "member agents",
			rawQuery: "q=needle&author_kind=member_agents&author_id=mem_1",
			want:     storemessage.MessageAuthorFilter{Kind: storemessage.MessageAuthorFilterMemberAgents, ID: "mem_1"},
		},
		{name: "unfiltered", rawQuery: "q=needle"},
		{name: "missing id", rawQuery: "q=needle&author_kind=member", wantError: "author_kind and author_id must be provided together"},
		{name: "missing kind", rawQuery: "q=needle&author_id=mem_1", wantError: "author_kind and author_id must be provided together"},
		{name: "empty pair", rawQuery: "q=needle&author_kind=&author_id=", wantError: "author_kind and author_id must be provided together"},
		{name: "unknown kind", rawQuery: "q=needle&author_kind=service&author_id=svc_1", wantError: "invalid author kind"},
		{name: "overlong id", rawQuery: "q=needle&author_kind=member&author_id=" + strings.Repeat("a", 256), wantError: "author id is too long"},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			queries := &recordingMessageQueryStore{}
			handler := &MessagesHandler{queries: queries}
			request := imWorkspaceRequest()
			request.Gin.Request = httptest.NewRequest(http.MethodGet, "/?"+testCase.rawQuery, nil)

			response, err := handler.Search(request)
			if testCase.wantError != "" {
				requireIMHTTPError(t, err, http.StatusBadRequest, testCase.wantError)
				require.Empty(t, queries.options)
				return
			}

			require.NoError(t, err)
			require.Equal(t, http.StatusOK, response.Status)
			require.Len(t, queries.options, 1)
			require.Equal(t, testCase.want, queries.options[0].AuthorFilter)
			if testCase.name == "member" {
				require.Equal(t, []string{"scp_1"}, queries.scopeIDs)
			}
		})
	}
}

func (fakeIMHumanWriter) ValidateHumanMessage(context.Context, identity.User, string, string, string, message.SendOptions) error {
	return nil
}

func (fakeIMHumanWriter) SendHumanMessage(context.Context, identity.User, string, string, string, []string, message.SendOptions) (storemessage.AppMessage, storemessage.MessageDelivery, error) {
	return storemessage.AppMessage{}, storemessage.MessageDelivery{}, nil
}

type recordingIMHumanWriter struct {
	sends         int
	workspaceID   string
	scopeID       string
	content       string
	attachmentIDs []string
	options       message.SendOptions
}

func (*recordingIMHumanWriter) ValidateHumanMessage(context.Context, identity.User, string, string, string, message.SendOptions) error {
	return nil
}

func (writer *recordingIMHumanWriter) SendHumanMessage(_ context.Context, _ identity.User, workspaceID string, scopeID string, content string, attachmentIDs []string, options message.SendOptions) (storemessage.AppMessage, storemessage.MessageDelivery, error) {
	writer.sends++
	writer.workspaceID = workspaceID
	writer.scopeID = scopeID
	writer.content = content
	writer.attachmentIDs = append([]string(nil), attachmentIDs...)
	writer.options = options
	createdAt := time.Date(2026, 6, 11, 10, 0, 0, 0, time.UTC)
	appMessage := storemessage.AppMessage{
		ID:              "msg_task_scope_1",
		ChannelID:       scopeID,
		ChannelType:     "task",
		ClientMessageID: options.ClientMessageID,
		Content:         content,
		Seq:             7,
		CreatedAt:       createdAt,
	}
	return appMessage, storemessage.MessageDelivery{
		ThreadID:        scopeID,
		MessageID:       appMessage.ID,
		ClientMessageID: options.ClientMessageID,
		Seq:             appMessage.Seq,
		AppMessage:      &appMessage,
		Message: storemessage.ReceiveMessage{
			MessageID: appMessage.ID,
			TargetKey: scopeID,
			TargetSeq: appMessage.Seq,
			Content:   content,
			Timestamp: createdAt,
		},
	}, nil
}

type recordingIMHumanEditor struct {
	fakeIMHumanWriter
	workspaceID  string
	scopeID      string
	messageID    string
	content      string
	mentionHints []storemessage.MessageMentionHint
	message      storemessage.AppMessage
}

func (editor *recordingIMHumanEditor) EditHumanMessage(_ context.Context, _ identity.User, workspaceID string, scopeID string, messageID string, content string, options message.EditOptions) (storemessage.AppMessage, error) {
	editor.workspaceID = workspaceID
	editor.scopeID = scopeID
	editor.messageID = messageID
	editor.content = content
	editor.mentionHints = options.MentionHints
	message := editor.message
	if message.ID == "" {
		editedAt := time.Date(2026, 6, 19, 10, 5, 0, 0, time.UTC)
		message = storemessage.AppMessage{
			ID:               messageID,
			Seq:              12,
			ChannelID:        scopeID,
			Content:          content,
			Metadata:         map[string]any{"mentions": []any{map[string]any{"type": "agent", "id": "agt_1", "token": "helper"}}},
			CreatedAt:        time.Date(2026, 6, 19, 10, 0, 0, 0, time.UTC),
			EditedAt:         &editedAt,
			EditedByMemberID: "mem_1",
			EditRevision:     1,
			AuthorProfile: storemessage.AuthorProfile{
				ActorType:   storemessage.AuthorActorTypeHuman,
				PrincipalID: "usr_1",
				MemberID:    "mem_1",
				DisplayName: "Owner",
			},
		}
	}
	return message, nil
}

func TestMessagesCreateTaskScopeUsesAtomicHumanWriterAndAcceptsClientMessageID(t *testing.T) {
	writer := &recordingIMHumanWriter{}
	streamWriter := &recordingStreamWriter{}
	handler := &MessagesHandler{
		writer:       writer,
		streamWriter: streamWriter,
	}

	response, err := handler.Create(imWorkspaceJSON(messageCreateRequest{
		Content:         "task update",
		AttachmentIDs:   []string{"att_1"},
		ClientMessageID: "client-task-1",
		Mentions: []dto.MessageMentionHint{{
			Type:  "agent",
			ID:    "agt_1",
			Token: "@nova",
		}},
	}, gin.Param{Key: "scopeId", Value: "tsk_123"}))

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, 1, writer.sends)
	require.Equal(t, "wsp_1", writer.workspaceID)
	require.Equal(t, "tsk_123", writer.scopeID)
	require.Equal(t, "task update", writer.content)
	require.Equal(t, []string{"att_1"}, writer.attachmentIDs)
	require.Equal(t, "client-task-1", writer.options.ClientMessageID)
	require.Equal(t, []storemessage.MessageMentionHint{{Type: "agent", ID: "agt_1", Token: "@nova"}}, writer.options.MentionHints)
	require.Len(t, streamWriter.scopeRequests, 1)
	require.Equal(t, "tsk_123", streamWriter.scopeRequests[0].ScopeID)
	require.Equal(t, imEventTypeMessageCreated, streamWriter.scopeRequests[0].Event.Type)
}

func TestMessagesCreateTaskScopeStillRejectsAgentThreadStart(t *testing.T) {
	writer := &recordingIMHumanWriter{}
	handler := &MessagesHandler{writer: writer}

	_, err := handler.Create(imWorkspaceJSON(messageCreateRequest{
		Content:          "task update",
		StartAgentThread: true,
	}, gin.Param{Key: "scopeId", Value: "tsk_123"}))

	require.Error(t, err)
	require.ErrorContains(t, err, "task scope messages cannot start agent threads")
	require.Zero(t, writer.sends)
}

func TestMessagesCreateRejectsReservedWorkflowMetadataBeforeWriteOrDelivery(t *testing.T) {
	writer := &recordingIMHumanWriter{}
	streamWriter := &recordingStreamWriter{}
	handler := &MessagesHandler{writer: writer, streamWriter: streamWriter}

	_, err := handler.Create(imWorkspaceJSON(messageCreateRequest{
		Content:  "forged callback authority",
		Metadata: map[string]any{storemessage.WorkflowContextMetadataEffectIDKey: "wfx_forged"},
	}, gin.Param{Key: "scopeId", Value: "chn_1"}))

	require.Error(t, err)
	require.ErrorContains(t, err, storemessage.WorkflowContextMetadataEffectIDKey)
	require.Zero(t, writer.sends)
	require.Empty(t, streamWriter.scopeRequests)
}

func TestMessagesUpdateEditsHumanMessageAndPublishesSharedEvent(t *testing.T) {
	editor := &recordingIMHumanEditor{}
	streamWriter := &recordingStreamWriter{}
	handler := &MessagesHandler{
		writer:       editor,
		streamWriter: streamWriter,
	}

	response, err := handler.Update(imWorkspaceJSON(messagePatchRequest{
		Content: " edited @helper ",
		Mentions: []dto.MessageMentionHint{{
			Type:  "agent",
			ID:    "agt_1",
			Token: "helper",
		}},
	}, gin.Param{Key: "scopeId", Value: "chn_1"}, gin.Param{Key: "messageId", Value: "msg_1"}))

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, "wsp_1", editor.workspaceID)
	require.Equal(t, "chn_1", editor.scopeID)
	require.Equal(t, "msg_1", editor.messageID)
	require.Equal(t, "edited @helper", editor.content)
	require.Equal(t, []storemessage.MessageMentionHint{{Type: "agent", ID: "agt_1", Token: "helper"}}, editor.mentionHints)

	body := response.Body.(dto.AppMessage)
	require.Equal(t, "msg_1", body.ID)
	require.Equal(t, "edited @helper", body.Content)

	require.Len(t, streamWriter.scopeRequests, 1)
	request := streamWriter.scopeRequests[0]
	require.Equal(t, "chn_1", request.ScopeID)
	require.Equal(t, imEventTypeMessageUpdated, request.Event.Type)
	require.Nil(t, request.Conversation)
	require.NotContains(t, request.Event.Payload, "message")
	require.Equal(t, "edited @helper", request.Event.Payload["content"])
	require.EqualValues(t, 1, request.Event.Payload["editRevision"])
}

func TestMessageMentionHintsFromDTOCapsPersistedHints(t *testing.T) {
	mentions := make([]dto.MessageMentionHint, 0, storemessage.MaxPersistedMentionHints+4)
	for i := 0; i < storemessage.MaxPersistedMentionHints+4; i++ {
		mentions = append(mentions, dto.MessageMentionHint{
			Type:  "agent",
			ID:    "agt_overflow",
			Token: "@agent",
		})
	}

	hints := messageMentionHintsFromDTO(mentions)

	require.Len(t, hints, storemessage.MaxPersistedMentionHints)
}
