package usecase_test

import (
	"context"
	"strings"
	"testing"

	"apps/backend/internal/module/ai"
	"apps/backend/internal/module/inspire/domain"
	"apps/backend/internal/module/inspire/repository"
	"apps/backend/internal/module/inspire/usecase"
	"apps/backend/internal/module/search"
	usageDomain "apps/backend/internal/module/usage/domain"
	usageRepo "apps/backend/internal/module/usage/repository"
	usageUC "apps/backend/internal/module/usage/usecase"

	"github.com/stretchr/testify/require"
)

func newInspire() *usecase.Service {
	svc := usecase.New(repository.NewMemory())
	ures := usageRepo.NewMemory()
	sr := &usageUC.StaticResolver{Map: map[string]string{}}
	us := usageUC.New(ures, sr)
	svc.Usage = us
	svc.AI = &ai.FakeClient{}
	svc.Search = &search.FakeClient{}
	svc.Personas = fakePersonaSource{}
	return svc
}

type fakePersonaSource struct{}

func (fakePersonaSource) ResolvePersona(context.Context, int64, string) (*usecase.PersonaSnapshot, error) {
	return &usecase.PersonaSnapshot{
		ID: "persona-ready", Name: "測試人設", Status: "ready",
		DraftText: "語氣自然、有具體情緒；句子長短交錯，不用固定開頭。",
	}, nil
}

type topicSearchStub struct{}

func (topicSearchStub) Search(context.Context, string, string, int) ([]search.Hit, error) {
	return []search.Hit{{
		Title:   "Threads 熱議：夏日防曬",
		Snippet: "#夏日防曬 最近討論增加",
	}}, nil
}

func setupInspireUID(svc *usecase.Service, uid int64) {
	_ = svc.Usage.Repo.SavePrefs(context.Background(), &usageDomain.MemberPrefs{
		UID: uid, PlanID: usageDomain.PlanPro, Unlimited: true, UpdatedAt: domain.NowNano(),
	})
	if sr, ok := svc.Usage.Resolver.(*usageUC.StaticResolver); ok {
		if sr.Map == nil {
			sr.Map = map[string]string{}
		}
		for _, m := range []string{usageDomain.MeterAICopy, usageDomain.MeterWebSearch, usageDomain.MeterAIImage} {
			sr.Map[itoa(uid)+":"+m] = usageDomain.KeyModePlatform
		}
	}
}

func itoa(n int64) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var b [32]byte
	i := len(b)
	for n > 0 {
		i--
		b[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		b[i] = '-'
	}
	return string(b[i:])
}

// Chat(ctx, uid, message, pinned, mode, persona, sessionID, material)
func chat(svc *usecase.Service, uid int64, msg, mode, sessionID, material string) (*usecase.ChatOutcome, error) {
	return svc.Chat(context.Background(), uid, msg, nil, mode, "", sessionID, material, false)
}

func TestIN_01_ListTrends(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_001)
	setupInspireUID(svc, uid)
	list, err := svc.ListTrends(context.Background(), uid)
	require.NoError(t, err)
	require.NotEmpty(t, list)
	require.NotEmpty(t, list[0].Label)
}

func TestIN_02_RefreshTrends(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_002)
	setupInspireUID(svc, uid)
	svc.Search = topicSearchStub{}
	svc.ResolveKey = func(context.Context, int64, string) (string, string, error) {
		return usageDomain.KeyModePlatform, "test-search-key", nil
	}
	_, _ = svc.ListTrends(context.Background(), uid)
	list, err := svc.RefreshTrends(context.Background(), uid)
	require.NoError(t, err)
	require.NotEmpty(t, list)
	require.Equal(t, "web", list[0].SourceLabel)

	// 相同搜尋結果不能再次冒充更新成功。
	_, err = svc.RefreshTrends(context.Background(), uid)
	require.Error(t, err)
}

func TestIN_02_RefreshFailureKeepsCacheAndDoesNotCharge(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_012)
	setupInspireUID(svc, uid)
	svc.Search = &search.FakeClient{Fail: true}
	svc.ResolveKey = func(context.Context, int64, string) (string, string, error) {
		return usageDomain.KeyModePlatform, "test-search-key", nil
	}
	before, err := svc.ListTrends(context.Background(), uid)
	require.NoError(t, err)

	_, err = svc.RefreshTrends(context.Background(), uid)
	require.Error(t, err)

	after, err := svc.ListTrends(context.Background(), uid)
	require.NoError(t, err)
	require.Equal(t, before[0].ID, after[0].ID)
	counter, err := svc.Usage.Repo.GetMonthlyCounter(context.Background(), uid, usageDomain.CurrentMonthKey())
	require.ErrorIs(t, err, usageDomain.ErrNotFound)
	require.Nil(t, counter)
}

func TestIN_03_ElementCRUD(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_003)
	setupInspireUID(svc, uid)
	el, err := svc.SaveElement(context.Background(), uid, &domain.Element{
		Kind: "snippet", Title: "鉤子", Body: "先講痛點", Reusable: true,
	})
	require.NoError(t, err)
	list, err := svc.ListElements(context.Background(), uid)
	require.NoError(t, err)
	require.Len(t, list, 1)
	require.NoError(t, svc.RemoveElement(context.Background(), uid, el.ID))
	list, _ = svc.ListElements(context.Background(), uid)
	require.Empty(t, list)
}

func TestIN_04_ChatMode(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_004)
	setupInspireUID(svc, uid)
	out, err := chat(svc, uid, "幫我想開場", "chat", "", "")
	require.NoError(t, err)
	require.NotNil(t, out)
	sess := out.Session
	require.GreaterOrEqual(t, len(sess.Messages), 2)
	require.Equal(t, "assistant", sess.Messages[len(sess.Messages)-1].Role)
	require.Nil(t, sess.Messages[len(sess.Messages)-1].Draft)
	require.NotEmpty(t, out.Fingerprint)
	require.Contains(t, out.Prompt, "發想")
}

func TestIN_05_GenerateUsesConversationAndPersona(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_005)
	setupInspireUID(svc, uid)
	first, err := chat(svc, uid, "遠端上班第三年，會議永遠開不完，我想談這種疲累感。", "chat", "", "")
	require.NoError(t, err)
	out, err := chat(svc, uid, "", "generate", first.Session.ID, "")
	require.NoError(t, err)
	last := out.Session.Messages[len(out.Session.Messages)-1]
	require.NotNil(t, last.Draft)
	require.NotEmpty(t, last.Draft.Body)
	require.Contains(t, out.Prompt, "對話素材")
	require.Contains(t, out.Prompt, "遠端上班第三年")
	require.Contains(t, out.Prompt, "測試人設")
	require.Contains(t, out.Prompt, "高互動貼文原則")
	user := out.Session.Messages[len(out.Session.Messages)-2]
	require.True(t, strings.HasPrefix(user.Text, "【產文】"))
}

func TestIN_05_ChatCanInjectExaResearch(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_015)
	setupInspireUID(svc, uid)

	out, err := svc.Chat(context.Background(), uid, "最近大家怎麼討論遠端工作？", nil, "chat", "", "", "", true)
	require.NoError(t, err)
	require.Contains(t, out.Prompt, "Exa 查詢資料")
	require.Contains(t, out.Prompt, "來源：")
}

func TestIN_07_PreviewMatchesChatFingerprint(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_007)
	setupInspireUID(svc, uid)
	msg := "寫一段開場鉤子"
	prev, err := svc.PreviewPrompt(context.Background(), uid, msg, nil, "chat", "", "", "")
	require.NoError(t, err)
	require.NotEmpty(t, prev.Fingerprint)
	out, err := chat(svc, uid, msg, "chat", "", "")
	require.NoError(t, err)
	require.Equal(t, prev.Fingerprint, out.Fingerprint, "preview and first chat must share fingerprint")
	require.Equal(t, prev.Prompt, out.Prompt)
}

func TestIN_06_ResearchSearch(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_006)
	setupInspireUID(svc, uid)
	hits, err := svc.ResearchSearch(context.Background(), uid, "敏感肌保養")
	require.NoError(t, err)
	require.NotEmpty(t, hits)
	require.NotEmpty(t, hits[0].URL)
}

// There is no image backend yet, so the endpoint must say so rather than charge for a
// placeholder avatar from a public URL.
func TestIN_07_GenerateImageReportsMissingBackend(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_017)
	setupInspireUID(svc, uid)
	img, err := svc.GenerateImage(context.Background(), uid, "溫暖海邊插畫")
	require.ErrorIs(t, err, domain.ErrNotImplemented)
	require.Nil(t, img)

	sum, err := svc.Usage.GetSummary(context.Background(), uid, "")
	require.NoError(t, err)
	require.Zero(t, sum.Platform.CreditsUsed, "an unimplemented feature must not be billed")
}

func TestIN_08_GenerateImageStillValidatesPrompt(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_008)
	setupInspireUID(svc, uid)
	_, err := svc.GenerateImage(context.Background(), uid, "   ")
	require.ErrorIs(t, err, domain.ErrValidation)
}

func TestIN_09_LegacyRemoved(t *testing.T) {
	svc := newInspire()
	require.ErrorIs(t, svc.LegacyRemoved(), domain.ErrRemoved)
}

func TestIN_10_ClearSessionCreatesNewKeepsOld(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_010)
	setupInspireUID(svc, uid)
	out, err := chat(svc, uid, "先聊一句", "chat", "", "")
	require.NoError(t, err)
	oldID := out.Session.ID
	require.NotEmpty(t, oldID)
	require.NotEmpty(t, out.Session.Messages)

	next, err := svc.ClearSession(context.Background(), uid)
	require.NoError(t, err)
	require.NotEqual(t, oldID, next.ID)
	require.Empty(t, next.Messages)

	list, err := svc.ListSessionSummaries(context.Background(), uid)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(list), 2)
	ids := map[string]bool{}
	for _, s := range list {
		ids[s.ID] = true
	}
	require.True(t, ids[oldID])
	require.True(t, ids[next.ID])

	old, err := svc.ActivateSession(context.Background(), uid, oldID)
	require.NoError(t, err)
	require.Equal(t, oldID, old.ID)
	require.NotEmpty(t, old.Messages)
}

func TestIN_11_MultiSessionSwitchAndDelete(t *testing.T) {
	svc := newInspire()
	uid := int64(5_001_011)
	setupInspireUID(svc, uid)
	a, err := chat(svc, uid, "話題A", "chat", "", "")
	require.NoError(t, err)
	b, err := svc.CreateSession(context.Background(), uid, "")
	require.NoError(t, err)
	require.NotEqual(t, a.Session.ID, b.ID)
	outB, err := chat(svc, uid, "話題B", "chat", b.ID, "")
	require.NoError(t, err)
	require.Equal(t, b.ID, outB.Session.ID)

	after, err := svc.DeleteSession(context.Background(), uid, b.ID)
	require.NoError(t, err)
	require.NotEqual(t, b.ID, after.ID)
	list, err := svc.ListSessionSummaries(context.Background(), uid)
	require.NoError(t, err)
	for _, s := range list {
		require.NotEqual(t, b.ID, s.ID)
	}
}

func TestIN_12_SessionIsolationAndHistoryKept(t *testing.T) {
	svc := newInspire()
	uidA := int64(5_001_012)
	uidB := int64(5_001_013)
	setupInspireUID(svc, uidA)
	setupInspireUID(svc, uidB)

	outA1, err := chat(svc, uidA, "A的第一則", "chat", "", "")
	require.NoError(t, err)
	sessA1 := outA1.Session.ID

	sA2, err := svc.CreateSession(context.Background(), uidA, "")
	require.NoError(t, err)
	outA2, err := chat(svc, uidA, "A的第二則對話", "chat", sA2.ID, "")
	require.NoError(t, err)
	require.Equal(t, sA2.ID, outA2.Session.ID)

	outB, err := chat(svc, uidB, "B的秘密話題", "chat", "", "")
	require.NoError(t, err)

	_, err = svc.GetSessionByID(context.Background(), uidB, sessA1)
	require.ErrorIs(t, err, domain.ErrNotFound)

	listA, err := svc.ListSessionSummaries(context.Background(), uidA)
	require.NoError(t, err)
	for _, s := range listA {
		require.NotEqual(t, outB.Session.ID, s.ID)
	}
	require.GreaterOrEqual(t, len(listA), 2)

	back, err := svc.ActivateSession(context.Background(), uidA, sessA1)
	require.NoError(t, err)
	require.Equal(t, sessA1, back.ID)
	found := false
	for _, m := range back.Messages {
		if m.Role == "user" && m.Text == "A的第一則" {
			found = true
		}
		require.NotEqual(t, "B的秘密話題", m.Text)
		require.NotEqual(t, "A的第二則對話", m.Text)
	}
	require.True(t, found)

	back2, err := svc.ActivateSession(context.Background(), uidA, sA2.ID)
	require.NoError(t, err)
	found2 := false
	for _, m := range back2.Messages {
		if m.Text == "A的第二則對話" {
			found2 = true
		}
		require.NotEqual(t, "A的第一則", m.Text)
	}
	require.True(t, found2)
}

func TestIN_13_ListTrendsDoesNotHitLiveSearch(t *testing.T) {
	svc := usecase.New(repository.NewMemory())
	uid := int64(5_001_014)
	list, err := svc.ListTrends(context.Background(), uid)
	require.NoError(t, err)
	require.NotEmpty(t, list)
	for _, titem := range list {
		require.Equal(t, "seed", titem.SourceLabel)
	}
}

func TestComposeDraftMaterial(t *testing.T) {
	sess := &domain.Session{
		Title: "周末咖啡",
		Messages: []domain.ChatMessage{
			{Role: "user", Text: "想寫周末放空"},
			{Role: "assistant", Text: "目前共識：慢節奏、不裝"},
			{Role: "user", Text: "【產文】短一點｜素材：xxx"},
		},
	}
	mat := usecase.ComposeDraftMaterial(sess, "")
	require.Contains(t, mat, "周末咖啡")
	require.Contains(t, mat, "想寫周末放空")
	require.NotContains(t, mat, "【產文】")
}
