package lark

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"

	gatewayapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/agent-gateway/gateway/v1"
	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"
	infralarkim "github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/im"
)

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

	gateway := NewMockGatewayServiceClient(gomock.NewController(t))
	ctx := &testContext{Context: context.Background()}
	resolver := func(callCtx context.Context, senderUserID string) (string, error) {
		require.Same(t, ctx, callCtx)
		require.Equal(t, "on_user", senderUserID)
		return "user@example.com", nil
	}
	lark := NewLark(gateway, infralarkim.NewMockIM(gomock.NewController(t)), resolver, testConfig())
	gateway.EXPECT().ProcessLarkMessage(gomock.Any(), gomock.Any()).DoAndReturn(
		func(callCtx context.Context, req *gatewayapi.ProcessLarkMessageRequest, _ ...grpc.CallOption) (*gatewayapi.ProcessLarkMessageResponse, error) {
			require.Same(t, ctx, callCtx)
			require.Equal(t, "ticket-manager", req.GetContext().GetInstanceId())
			require.Equal(t, "/workspace", req.GetContext().GetWorkingDirectory())
			require.Equal(t, "om_1", req.GetMessage().GetMessageId())
			require.Equal(t, "om_root", req.GetMessage().GetRootId())
			require.Equal(t, "omt_thread", req.GetMessage().GetThreadId())
			require.Equal(t, "oc_chat", req.GetMessage().GetChatId())
			require.Equal(t, "group", req.GetMessage().GetChatType())
			require.Equal(t, "text", req.GetMessage().GetMessageType())
			require.JSONEq(t, `{"text":"@_user_1 investigate"}`, req.GetMessage().GetContent())
			require.Equal(t, "user", req.GetMessage().GetSenderType())
			require.Equal(t, "user@example.com", req.GetMessage().GetSenderEmail())
			require.Equal(t, []*gatewayv1.LarkMention{{Key: "@_user_1", OpenId: "ou_bot"}}, req.GetMessage().GetMentions())
			return &gatewayapi.ProcessLarkMessageResponse{
				Disposition: gatewayapi.ProcessLarkMessageDisposition_PROCESS_LARK_MESSAGE_DISPOSITION_ACCEPTED,
				Task:        &sessionv1.Task{TaskId: "task-1"},
			}, nil
		},
	)

	err := lark.HandleLarkMessage(ctx, newMessageEvent(t, larkEventData{
		MessageID:  "om_1",
		RootID:     "om_root",
		ThreadID:   "omt_thread",
		ChatID:     "oc_chat",
		ChatType:   "group",
		Text:       "@_user_1 investigate",
		SenderType: "user",
		Mentions:   botMentions(),
	}))
	require.NoError(t, err)
}

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

	gateway := NewMockGatewayServiceClient(gomock.NewController(t))
	content := `{"zh_cn":{"title":"Incident","content":[[{"tag":"at","user_id":"ou_bot","user_name":"Agent"},{"tag":"text","text":"检查 "},{"tag":"a","text":"TICKET-1","href":"https://example.com/TICKET-1"}]]}}`
	gateway.EXPECT().ProcessLarkMessage(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, req *gatewayapi.ProcessLarkMessageRequest, _ ...grpc.CallOption) (*gatewayapi.ProcessLarkMessageResponse, error) {
			require.Equal(t, "post", req.GetMessage().GetMessageType())
			require.JSONEq(t, content, req.GetMessage().GetContent())
			require.Equal(t, []*gatewayv1.LarkMention{{Key: "@_user_1", OpenId: "ou_bot"}}, req.GetMessage().GetMentions())
			return &gatewayapi.ProcessLarkMessageResponse{Task: &sessionv1.Task{TaskId: "task-1"}}, nil
		},
	)
	lark := NewLark(
		gateway,
		infralarkim.NewMockIM(gomock.NewController(t)),
		func(context.Context, string) (string, error) { return "user@example.com", nil },
		testConfig(),
	)

	err := lark.HandleLarkMessage(context.Background(), newMessageEvent(t, larkEventData{
		ChatType:    "group",
		MessageType: "post",
		Content:     content,
		Mentions:    botMentions(),
	}))
	require.NoError(t, err)
}

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

	tests := []struct {
		name      string
		configure func(*LarkConfig)
		data      larkEventData
	}{
		{name: "app sender", data: larkEventData{SenderType: "app"}},
		{name: "non text", data: larkEventData{MessageType: "image"}},
		{
			name: "group disabled",
			configure: func(config *LarkConfig) {
				config.AllowGroupChat = false
			},
			data: larkEventData{ChatType: "group", Mentions: botMentions()},
		},
		{name: "group without bot mention", data: larkEventData{ChatType: "group"}},
		{
			name: "message too long",
			configure: func(config *LarkConfig) {
				config.MaxMessageSize = 3
			},
			data: larkEventData{Text: "一二三四"},
		},
		{
			name: "post message too long",
			configure: func(config *LarkConfig) {
				config.MaxMessageSize = 3
			},
			data: larkEventData{
				MessageType: "post",
				Content:     `{"title":"","content":[[{"tag":"text","text":"一二三四"}]]}`,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			config := testConfig()
			if tt.configure != nil {
				tt.configure(&config)
			}
			lark := NewLark(
				NewMockGatewayServiceClient(gomock.NewController(t)),
				infralarkim.NewMockIM(gomock.NewController(t)),
				func(context.Context, string) (string, error) { return "user@example.com", nil },
				config,
			)
			require.NoError(t, lark.HandleLarkMessage(context.Background(), newMessageEvent(t, tt.data)))
		})
	}
}

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

	t.Run("invalid content", func(t *testing.T) {
		t.Parallel()
		lark := NewLark(
			NewMockGatewayServiceClient(gomock.NewController(t)),
			infralarkim.NewMockIM(gomock.NewController(t)),
			func(context.Context, string) (string, error) { return "user@example.com", nil },
			testConfig(),
		)
		event := newMessageEvent(t, larkEventData{})
		badContent := "{"
		event.Event.Message.Content = &badContent
		require.ErrorContains(t, lark.HandleLarkMessage(context.Background(), event), "decode lark text message")
	})

	t.Run("invalid post content", func(t *testing.T) {
		t.Parallel()
		lark := NewLark(
			NewMockGatewayServiceClient(gomock.NewController(t)),
			infralarkim.NewMockIM(gomock.NewController(t)),
			func(context.Context, string) (string, error) { return "user@example.com", nil },
			testConfig(),
		)
		event := newMessageEvent(t, larkEventData{MessageType: "post", Content: "{"})
		require.ErrorContains(t, lark.HandleLarkMessage(context.Background(), event), "decode lark post message")
	})

	t.Run("resolver failure", func(t *testing.T) {
		t.Parallel()
		want := errors.New("user unavailable")
		lark := NewLark(
			NewMockGatewayServiceClient(gomock.NewController(t)),
			infralarkim.NewMockIM(gomock.NewController(t)),
			func(context.Context, string) (string, error) { return "", want },
			testConfig(),
		)
		require.ErrorIs(t, lark.HandleLarkMessage(context.Background(), newMessageEvent(t, larkEventData{})), want)
	})

	t.Run("gateway failure", func(t *testing.T) {
		t.Parallel()
		gateway := NewMockGatewayServiceClient(gomock.NewController(t))
		want := errors.New("gateway unavailable")
		gateway.EXPECT().ProcessLarkMessage(gomock.Any(), gomock.Any()).Return(nil, want)
		lark := NewLark(
			gateway,
			infralarkim.NewMockIM(gomock.NewController(t)),
			func(context.Context, string) (string, error) { return "user@example.com", nil },
			testConfig(),
		)
		require.ErrorIs(t, lark.HandleLarkMessage(context.Background(), newMessageEvent(t, larkEventData{})), want)
	})
}

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

	gateway := NewMockGatewayServiceClient(gomock.NewController(t))
	gateway.EXPECT().ProcessLarkMessage(gomock.Any(), gomock.Any()).Return(&gatewayapi.ProcessLarkMessageResponse{
		Disposition: gatewayapi.ProcessLarkMessageDisposition_PROCESS_LARK_MESSAGE_DISPOSITION_DUPLICATED,
		Task:        &sessionv1.Task{TaskId: "task-2", SessionId: "session-1"},
	}, nil)
	larkBot := NewLark(
		gateway,
		infralarkim.NewMockIM(gomock.NewController(t)),
		func(context.Context, string) (string, error) { return "user@example.com", nil },
		testConfig(),
	).(*lark)
	larkBot.reactor.commentaryMessages.Store("session-1", cachedCommentaryMessage{taskID: "task-1"})

	require.NoError(t, larkBot.HandleLarkMessage(context.Background(), newMessageEvent(t, larkEventData{})))
	_, ok := larkBot.reactor.commentaryMessages.Load("session-1")
	require.False(t, ok)
}

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

	ctrl := gomock.NewController(t)
	lark := infralarkim.NewMockIM(ctrl)
	renderer := NewMockRenderer(ctrl)
	card := infralarkim.RecvMessageText{Text: "custom result"}
	replyID := "reply-1"
	renderer.EXPECT().Result("task-1", "done").Return(card, nil)
	lark.EXPECT().ReplyMessage(gomock.Any(), "om_source", card, true).Return(&replyID, nil)
	larkAdapter := NewLark(
		NewMockGatewayServiceClient(ctrl),
		lark,
		func(context.Context, string) (string, error) { return "user@example.com", nil },
		testConfig(),
		WithRenderer(renderer),
	)

	require.NoError(t, larkAdapter.HandleLarkTaskEvent(context.Background(), newTaskEvent(
		gatewayv1.TaskEventType_TASK_EVENT_TYPE_FINISHED,
		sessionv1.TaskStatus_TASK_STATUS_SUCCEEDED,
		"done",
	)))
}

func testConfig() LarkConfig {
	return LarkConfig{
		InstanceID:                   "ticket-manager",
		WorkingDirectory:             "/workspace",
		BotOpenID:                    "ou_bot",
		AllowGroupChat:               true,
		MaxMessageSize:               8000,
		CommentaryMessageMergeBuffer: 4000,
	}
}

type testContext struct {
	context.Context
}

type larkEventData struct {
	MessageID    string
	RootID       string
	ThreadID     string
	ChatID       string
	ChatType     string
	MessageType  string
	Content      string
	Text         string
	SenderUserID string
	SenderType   string
	Mentions     []map[string]any
}

func newMessageEvent(t *testing.T, data larkEventData) *larkim.P2MessageReceiveV1 {
	t.Helper()
	if data.MessageID == "" {
		data.MessageID = "om_default"
	}
	if data.ChatID == "" {
		data.ChatID = "oc_default"
	}
	if data.ChatType == "" {
		data.ChatType = "p2p"
	}
	if data.MessageType == "" {
		data.MessageType = "text"
	}
	if data.Text == "" {
		data.Text = "investigate"
	}
	if data.SenderUserID == "" {
		data.SenderUserID = "on_user"
	}
	content := data.Content
	if content == "" {
		encoded, err := json.Marshal(map[string]string{"text": data.Text})
		require.NoError(t, err)
		content = string(encoded)
	}
	payload := map[string]any{
		"event": map[string]any{
			"sender": map[string]any{
				"sender_id":   map[string]any{"user_id": data.SenderUserID, "open_id": "ou_user"},
				"sender_type": data.SenderType,
			},
			"message": map[string]any{
				"message_id":   data.MessageID,
				"root_id":      data.RootID,
				"thread_id":    data.ThreadID,
				"chat_id":      data.ChatID,
				"chat_type":    data.ChatType,
				"message_type": data.MessageType,
				"content":      content,
				"mentions":     data.Mentions,
			},
		},
	}
	raw, err := json.Marshal(payload)
	require.NoError(t, err)
	event := &larkim.P2MessageReceiveV1{}
	require.NoError(t, json.Unmarshal(raw, event))
	return event
}

func botMentions() []map[string]any {
	return []map[string]any{{"key": "@_user_1", "id": map[string]any{"open_id": "ou_bot"}}}
}
