package app

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"testing"
	"time"

	larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
	"github.com/stretchr/testify/require"
)

type recordingNotifier struct {
	mu            sync.Mutex
	replies       []string
	sends         []string
	sendChatIDs   []string
	failSendCount int
}

func (n *recordingNotifier) Reply(_ context.Context, _ string, text string) error {
	n.mu.Lock()
	defer n.mu.Unlock()
	n.replies = append(n.replies, text)
	return nil
}

func (n *recordingNotifier) ReplyOnce(ctx context.Context, messageID, _ string, text string) error {
	return n.Reply(ctx, messageID, text)
}

func (n *recordingNotifier) Send(_ context.Context, chatID, _ string, text string) error {
	n.mu.Lock()
	defer n.mu.Unlock()
	if n.failSendCount > 0 {
		n.failSendCount--
		return fmt.Errorf("temporary send failure")
	}
	n.sendChatIDs = append(n.sendChatIDs, chatID)
	n.sends = append(n.sends, text)
	return nil
}

type blockingRunner struct{}

func (blockingRunner) Run(ctx context.Context, _ Job) (RunResult, error) {
	<-ctx.Done()
	return RunResult{}, ctx.Err()
}

type recordingFeedbackReminder struct {
	called chan string
}

func (r *recordingFeedbackReminder) Run(_ context.Context, idempotencyKey string) (FeedbackReminderResult, error) {
	r.called <- idempotencyKey
	return FeedbackReminderResult{TicketCount: 3, AssigneeCount: 2, Sent: true}, nil
}

func TestLarkIngressAllowsAnySenderInAllowedGroupWithExactMention(t *testing.T) {
	store, err := OpenStore(filepathForTest(t))
	require.NoError(t, err)
	notifier := &recordingNotifier{}
	worker := NewWorker(store, blockingRunner{}, notifier, time.Hour, time.Hour, "")
	ingress := NewLarkIngress(Config{
		LarkBotOpenID: "ou_bot", AllowedChatIDs: map[string]struct{}{"oc_allowed": {}},
	}, worker, notifier)

	event := larkEvent("/pr fix the bug", "ou_bot")
	event.Event.Sender.SenderId.OpenId = pointer("ou_unlisted_user")
	require.NoError(t, ingress.Handle(context.Background(), event))
	ids := store.QueuedIDs()
	require.Len(t, ids, 1)
	job, ok := store.Get(ids[0])
	require.True(t, ok)
	require.Equal(t, JobModePR, job.Mode)
	require.Equal(t, "fix the bug", job.Request)

	wrongMention := larkEvent("diagnose", "ou_other")
	wrongMention.Event.Message.MessageId = pointer("om_2")
	require.NoError(t, ingress.Handle(context.Background(), wrongMention))
	require.Len(t, store.QueuedIDs(), 1)

	wrongChat := larkEvent("diagnose", "ou_bot")
	wrongChat.Event.Message.MessageId = pointer("om_3")
	wrongChat.Event.Message.ChatId = pointer("oc_unlisted_chat")
	require.NoError(t, ingress.Handle(context.Background(), wrongChat))
	require.Len(t, store.QueuedIDs(), 1)
}

func TestLarkIngressKeepsP2PPRAllowlist(t *testing.T) {
	store, err := OpenStore(filepathForTest(t))
	require.NoError(t, err)
	notifier := &recordingNotifier{}
	worker := NewWorker(store, blockingRunner{}, notifier, time.Hour, time.Hour, "")
	ingress := NewLarkIngress(Config{
		LarkBotOpenID:        "ou_bot",
		AllowedSenderOpenIDs: map[string]struct{}{"ou_user": {}}, PRSenderOpenIDs: map[string]struct{}{"ou_owner": {}},
	}, worker, notifier)

	event := larkEvent("/pr change code", "")
	event.Event.Message.ChatType = pointer("p2p")
	event.Event.Message.Mentions = nil
	content, err := json.Marshal(map[string]string{"text": "/pr change code"})
	require.NoError(t, err)
	event.Event.Message.Content = pointer(string(content))
	require.NoError(t, ingress.Handle(context.Background(), event))
	require.Empty(t, store.QueuedIDs())
	require.Contains(t, notifier.replies[0], "没有提交 PR 的权限")
}

func TestLarkIngressAllowsAuthorizedP2PWithoutChatAllowlist(t *testing.T) {
	store, err := OpenStore(filepathForTest(t))
	require.NoError(t, err)
	notifier := &recordingNotifier{}
	worker := NewWorker(store, blockingRunner{}, notifier, time.Hour, time.Hour, "")
	ingress := NewLarkIngress(Config{
		LarkBotOpenID: "ou_bot", AllowedSenderOpenIDs: map[string]struct{}{"ou_user": {}},
		PRSenderOpenIDs: map[string]struct{}{"ou_user": {}},
	}, worker, notifier)
	event := larkEvent("diagnose this", "")
	event.Event.Message.ChatType = pointer("p2p")
	event.Event.Message.Mentions = nil
	content, err := json.Marshal(map[string]string{"text": "diagnose this"})
	require.NoError(t, err)
	event.Event.Message.Content = pointer(string(content))
	require.NoError(t, ingress.Handle(context.Background(), event))
	require.Len(t, store.QueuedIDs(), 1)
	job, ok := store.Get(store.QueuedIDs()[0])
	require.True(t, ok)
	require.Empty(t, job.LarkRootMessageID)
}

func TestLarkIngressRecordsRootMessageForThreadContinuation(t *testing.T) {
	store, err := OpenStore(filepathForTest(t))
	require.NoError(t, err)
	notifier := &recordingNotifier{}
	worker := NewWorker(store, blockingRunner{}, notifier, time.Hour, time.Hour, "")
	ingress := NewLarkIngress(Config{
		LarkBotOpenID: "ou_bot", AllowedChatIDs: map[string]struct{}{"oc_allowed": {}},
	}, worker, notifier)

	root := larkEvent("first request", "ou_bot")
	require.NoError(t, ingress.Handle(context.Background(), root))
	first, ok := store.Get(store.QueuedIDs()[0])
	require.True(t, ok)
	require.Equal(t, "om_1", first.LarkRootMessageID)

	followUp := larkEvent("continue checking", "ou_bot")
	followUp.Event.Message.MessageId = pointer("om_2")
	followUp.Event.Message.RootId = pointer("om_1")
	require.NoError(t, ingress.Handle(context.Background(), followUp))
	jobs := store.QueuedIDs()
	require.Len(t, jobs, 2)
	second, ok := store.Get(jobs[1])
	require.True(t, ok)
	require.Equal(t, "om_1", second.LarkRootMessageID)
}

func TestLarkIngressCreatesSingleTicketJobWithIssueBoundary(t *testing.T) {
	store, err := OpenStore(filepathForTest(t))
	require.NoError(t, err)
	notifier := &recordingNotifier{}
	worker := NewWorker(store, blockingRunner{}, notifier, time.Hour, time.Hour, "")
	ingress := NewLarkIngress(Config{
		LarkBotOpenID: "ou_bot", AllowedChatIDs: map[string]struct{}{"oc_allowed": {}},
	}, worker, notifier)

	event := larkEvent("/ticket apid-29232 facts disappear after timeout", "ou_bot")
	require.NoError(t, ingress.Handle(context.Background(), event))
	ids := store.QueuedIDs()
	require.Len(t, ids, 1)
	job, ok := store.Get(ids[0])
	require.True(t, ok)
	require.Equal(t, JobModeTicket, job.Mode)
	require.Equal(t, "APID-29232", job.IssueKey)
	require.Contains(t, notifier.replies[0], "确定性工程问题")
}

func TestLarkIngressRunsExactFeedbackReminderCommandInConfiguredGroup(t *testing.T) {
	store, err := OpenStore(filepathForTest(t))
	require.NoError(t, err)
	notifier := &recordingNotifier{}
	worker := NewWorker(store, blockingRunner{}, notifier, time.Hour, time.Hour, "")
	reminder := &recordingFeedbackReminder{called: make(chan string, 1)}
	ingress := NewLarkIngress(Config{
		LarkBotOpenID: "ou_bot", AllowedChatIDs: map[string]struct{}{"oc_allowed": {}},
		FeedbackReminderChatID: "oc_allowed",
	}, worker, notifier, reminder)

	event := larkEvent("/fb-reminder", "ou_bot")
	require.NoError(t, ingress.Handle(context.Background(), event))
	select {
	case idempotencyKey := <-reminder.called:
		require.Contains(t, idempotencyKey, "fb-reminder-manual:")
	case <-time.After(time.Second):
		t.Fatal("feedback reminder was not triggered")
	}
	require.Eventually(t, func() bool {
		notifier.mu.Lock()
		defer notifier.mu.Unlock()
		return len(notifier.replies) == 2
	}, time.Second, 10*time.Millisecond)
	require.Empty(t, store.QueuedIDs())
	require.Contains(t, notifier.replies[0], "正在生成")
	require.Contains(t, notifier.replies[1], "3 张工单")
}

func larkEvent(text, mentionOpenID string) *larkim.P2MessageReceiveV1 {
	content, _ := json.Marshal(map[string]string{"text": "@_user_1 " + text})
	return &larkim.P2MessageReceiveV1{Event: &larkim.P2MessageReceiveV1Data{
		Sender: &larkim.EventSender{SenderId: &larkim.UserId{OpenId: pointer("ou_user")}},
		Message: &larkim.EventMessage{
			MessageId: pointer("om_1"), ChatId: pointer("oc_allowed"), ChatType: pointer("group"),
			MessageType: pointer("text"), Content: pointer(string(content)),
			Mentions: []*larkim.MentionEvent{{Key: pointer("@_user_1"), Id: &larkim.UserId{OpenId: pointer(mentionOpenID)}}},
		},
	}}
}

func filepathForTest(t *testing.T) string {
	return t.TempDir() + "/jobs.json"
}

func pointer(value string) *string { return &value }
