package lark

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	gatewayv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/gateway/v1"
	sessionv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/session/v1"
	gatewayerrors "github.com/tidbcloud/aisupport/packages/sdk-go/infra/agentgateway/errors"
	infralarkim "github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/im"
)

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 7, lark)
	firstCardID := "card-1"
	secondCardID := "card-2"
	gomock.InOrder(
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).DoAndReturn(
			func(_ context.Context, _ string, message infralarkim.Message, _ bool) (*string, error) {
				requireCardContains(t, message, `"content":"一"`)
				return &firstCardID, nil
			},
		),
		lark.EXPECT().PatchMessage(gomock.Any(), firstCardID, gomock.Any()).DoAndReturn(
			func(_ context.Context, _ string, message infralarkim.Message) error {
				requireCardContains(t, message, `"content":"一\n---\n二"`)
				requireCardContains(t, message, `"content":"\u003c 二 ..."`)
				return nil
			},
		),
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).DoAndReturn(
			func(_ context.Context, _ string, message infralarkim.Message, _ bool) (*string, error) {
				requireCardContains(t, message, `"content":"三"`)
				return &secondCardID, nil
			},
		),
		lark.EXPECT().PatchMessage(gomock.Any(), secondCardID, gomock.Any()).DoAndReturn(
			func(_ context.Context, _ string, message infralarkim.Message) error {
				requireCardContains(t, message, `"content":"三\n---\n四"`)
				return nil
			},
		),
	)

	for _, content := range []string{"一", "二", "三", "四"} {
		require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", content)))
	}

	cached, ok := reactor.commentaryMessages.Load("session-1")
	require.True(t, ok)
	require.Equal(t, cachedCommentaryMessage{taskID: "task-1", messageID: secondCardID, content: "三\n---\n四"}, cached)
}

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 3, lark)
	cardID := "card-1"
	lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).DoAndReturn(
		func(_ context.Context, _ string, message infralarkim.Message, _ bool) (*string, error) {
			requireCardContains(t, message, `"content":"一二三四"`)
			return &cardID, nil
		},
	)

	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "一二三四")))
	cached, ok := reactor.commentaryMessages.Load("session-1")
	require.True(t, ok)
	require.Equal(t, "一二三四", cached.content)
}

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 100, lark)
	firstCardID := "card-1"
	secondCardID := "card-2"
	gomock.InOrder(
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&firstCardID, nil),
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).DoAndReturn(
			func(_ context.Context, _ string, message infralarkim.Message, _ bool) (*string, error) {
				requireCardContains(t, message, `"content":"second"`)
				return &secondCardID, nil
			},
		),
	)

	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "first")))
	reactor.ClearCommentaryMessageBuffer("session-1")
	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "second")))
}

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 100, lark)
	firstCardID := "card-1"
	secondCardID := "card-2"
	gomock.InOrder(
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&firstCardID, nil),
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&secondCardID, nil),
	)

	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "first")))
	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-2", "session-1", "second")))

	cached, ok := reactor.commentaryMessages.Load("session-1")
	require.True(t, ok)
	require.Equal(t, "task-2", cached.taskID)
}

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

	t.Run("reply", func(t *testing.T) {
		t.Parallel()
		lark := infralarkim.NewMockIM(gomock.NewController(t))
		reactor := newReactor("ticket-manager", 100, lark)
		cardID := "card-1"
		gomock.InOrder(
			lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(nil, errors.New("send failed")),
			lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&cardID, nil),
		)

		err := reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "first"))
		require.ErrorContains(t, err, "send failed")
		_, ok := reactor.commentaryMessages.Load("session-1")
		require.False(t, ok)
		require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "second")))
	})

	t.Run("patch", func(t *testing.T) {
		t.Parallel()
		lark := infralarkim.NewMockIM(gomock.NewController(t))
		reactor := newReactor("ticket-manager", 100, lark)
		cardID := "card-1"
		gomock.InOrder(
			lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&cardID, nil),
			lark.EXPECT().PatchMessage(gomock.Any(), cardID, gomock.Any()).Return(errors.New("patch failed")),
			lark.EXPECT().PatchMessage(gomock.Any(), cardID, gomock.Any()).DoAndReturn(
				func(_ context.Context, _ string, message infralarkim.Message) error {
					requireCardContains(t, message, `"content":"first\n---\nthird"`)
					return nil
				},
			),
		)

		require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "first")))
		err := reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "second"))
		require.ErrorContains(t, err, "patch failed")
		require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "third")))
	})
}

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 100, lark)
	reactionID := "reaction-1"
	commentaryCardID := "commentary-card"
	resultCardID := "result-card"
	gomock.InOrder(
		lark.EXPECT().ReactEmoji(gomock.Any(), "om_source", larkProcessingReactionEmoji).Return(&reactionID, nil),
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&commentaryCardID, nil),
		lark.EXPECT().UnreactEmoji(gomock.Any(), "om_source", reactionID).Return(nil),
		lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).DoAndReturn(
			func(_ context.Context, _ string, message infralarkim.Message, _ bool) (*string, error) {
				requireCardContains(t, message, `"content":"Result"`)
				return &resultCardID, nil
			},
		),
	)

	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), newTaskEvent(
		gatewayv1.TaskEventType_TASK_EVENT_TYPE_STARTING, sessionv1.TaskStatus_TASK_STATUS_PROCESSING, "",
	)))
	storedReactionID, ok := reactor.emojiReactions.Load("task-1")
	require.True(t, ok)
	require.Equal(t, reactionID, storedReactionID)
	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-1", "session-1", "checking")))
	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), newTaskEvent(
		gatewayv1.TaskEventType_TASK_EVENT_TYPE_FINISHED, sessionv1.TaskStatus_TASK_STATUS_SUCCEEDED, "done",
	)))
	_, ok = reactor.emojiReactions.Load("task-1")
	require.False(t, ok)
	_, ok = reactor.commentaryMessages.Load("session-1")
	require.False(t, ok)
}

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 100, lark)
	commentaryCardID := "commentary-card"
	resultCardID := "result-card"
	lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&commentaryCardID, nil)
	lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&resultCardID, nil)

	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-2", "session-1", "new task")))
	finished := newTaskEvent(gatewayv1.TaskEventType_TASK_EVENT_TYPE_FINISHED, sessionv1.TaskStatus_TASK_STATUS_SUCCEEDED, "old result")
	finished.TaskId = "task-1"
	require.NoError(t, reactor.HandleAgentTaskEvent(context.Background(), finished))

	cached, ok := reactor.commentaryMessages.Load("session-1")
	require.True(t, ok)
	require.Equal(t, "task-2", cached.taskID)
}

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

	reactor := newReactor("ticket-manager", 100, infralarkim.NewMockIM(gomock.NewController(t)))
	event := commentaryEvent("task-1", "", "commentary")
	err := reactor.HandleAgentTaskEvent(context.Background(), event)
	require.ErrorIs(t, err, gatewayerrors.ErrInvalidTaskEvent)
}

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

	lark := infralarkim.NewMockIM(gomock.NewController(t))
	reactor := newReactor("ticket-manager", 100, lark)
	cardID := "card"
	const sessionCount = 16
	lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", gomock.Any(), true).Return(&cardID, nil).Times(sessionCount)

	errs := make(chan error, sessionCount)
	var wg sync.WaitGroup
	for i := range sessionCount {
		wg.Add(1)
		go func() {
			defer wg.Done()
			id := fmt.Sprintf("%d", i)
			errs <- reactor.HandleAgentTaskEvent(context.Background(), commentaryEvent("task-"+id, "session-"+id, "commentary"))
		}()
	}
	wg.Wait()
	close(errs)
	for err := range errs {
		require.NoError(t, err)
	}
	for i := range sessionCount {
		_, ok := reactor.commentaryMessages.Load(fmt.Sprintf("session-%d", i))
		require.True(t, ok)
	}
}

func commentaryEvent(taskID, sessionID, content string) *gatewayv1.TaskEvent {
	event := newTaskEvent(
		gatewayv1.TaskEventType_TASK_EVENT_TYPE_AGENT_COMMENTARY,
		sessionv1.TaskStatus_TASK_STATUS_PROCESSING,
		content,
	)
	event.TaskId = taskID
	event.SessionId = sessionID
	return event
}

func requireCardContains(t *testing.T, message infralarkim.Message, content string) {
	t.Helper()
	encoded, err := infralarkim.EncodeSendMessage(message)
	require.NoError(t, err)
	require.Contains(t, encoded, content)
}

func newTaskEvent(eventType gatewayv1.TaskEventType, status sessionv1.TaskStatus, content string) *gatewayv1.TaskEvent {
	return &gatewayv1.TaskEvent{
		Type:      eventType,
		TaskId:    "task-1",
		SessionId: "session-1",
		SessionSource: &sessionv1.SessionSource{
			Source: &sessionv1.MessageSourceRef{
				Type:       sessionv1.MessageSourceType_MESSAGE_SOURCE_TYPE_LARK,
				InstanceId: "ticket-manager",
			},
			SourceSessionKey: "chat-1:om_source",
		},
		TaskSource: &sessionv1.TaskSource{SourceMessageId: "om_source", SenderEmail: "user@example.com"},
		Status:     status,
		Content:    content,
	}
}
