package radar

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"testing"

	"apps/backend/internal/middleware"
	radarRepo "apps/backend/internal/module/radar/repository"
	radarUC "apps/backend/internal/module/radar/usecase"
	usageDomain "apps/backend/internal/module/usage/domain"
	usageRepo "apps/backend/internal/module/usage/repository"
	usageUC "apps/backend/internal/module/usage/usecase"
	"apps/backend/internal/svc"
	"apps/backend/internal/types"
)

/*
M1 驗收（spec §9.1）：SP-01、SP-02、RW-01～RW-04。

這裡走 handler 呼叫的 logic 層與真的 usecase／repository，只有兩處換成假的：
儲存層用 memory（不需要 mongo 就能跑）、AI 用 fakeSuggestAI（不打 provider、不花錢）。
每個 case 的失敗訊息都帶 spec ID，壞掉時能直接對回驗收表。
*/

type fakeSuggestAI struct {
	reply  string
	err    error
	calls  int
	prompt string
}

func (f *fakeSuggestAI) Complete(_ context.Context, _, _, prompt string) (string, error) {
	f.calls++
	f.prompt = prompt
	return f.reply, f.err
}

func (f *fakeSuggestAI) CompleteStream(ctx context.Context, apiKey, model, prompt string, _ func(string) error) (string, error) {
	return f.Complete(ctx, apiKey, model, prompt)
}

func (f *fakeSuggestAI) ListModels(context.Context, string) ([]string, error) { return nil, nil }

// 假 AI 回傳必須通過 Threads 短詞規則（IsThreadsSearchable）：
// include ≤2 token、中文每詞 2–4 字；exclude 仍走較寬長度界線。
const fakeSuggestReply = `[
  {"term":"婚攝 求推薦","reason":"正在找婚禮攝影的人最常這樣問","usage":"include"},
  {"term":"台北 婚攝","reason":"問價格的人通常已經在比較廠商","usage":"include"},
  {"term":"徵婚攝","reason":"這是同業徵才，不是客戶需求","usage":"exclude"}
]`

type m1Env struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	ai     *fakeSuggestAI
	usage  *usageUC.Service
	uid    int64
}

func newM1Env(t *testing.T, uid int64, maxActive int) *m1Env {
	t.Helper()
	ai := &fakeSuggestAI{reply: fakeSuggestReply}
	key := func(meter string) string { return fmt.Sprintf("%d:%s", uid, meter) }
	usage := usageUC.New(usageRepo.NewMemory(), &usageUC.StaticResolver{Map: map[string]string{
		key(usageDomain.MeterAICopy):     usageDomain.KeyModePlatform,
		key(usageDomain.MeterAIResearch): usageDomain.KeyModePlatform,
	}})

	radar := radarUC.New(radarRepo.NewMemory())
	radar.Quota = radarUC.FixedQuota{MaxActiveWatches: maxActive, MaxDailyOpportunities: 30}
	radar.AI = ai
	radar.Usage = usage

	return &m1Env{
		ctx:    middleware.WithUID(context.Background(), uid),
		svcCtx: &svc.ServiceContext{Radar: radar},
		ai:     ai,
		usage:  usage,
		uid:    uid,
	}
}

func (e *m1Env) seedProfile(t *testing.T) {
	t.Helper()
	if _, err := NewUpsertServiceProfileLogic(e.ctx, e.svcCtx).UpsertServiceProfile(validProfileReq()); err != nil {
		t.Fatalf("seed service profile: %v", err)
	}
}

func (e *m1Env) createWatch(t *testing.T, term string, enabled bool) *types.RadarWatchPublic {
	t.Helper()
	w, err := NewCreateWatchLogic(e.ctx, e.svcCtx).CreateWatch(&types.CreateWatchReq{
		Terms:   []string{term},
		Enabled: enabled,
	})
	if err != nil {
		t.Fatalf("create watch %q: %v", term, err)
	}
	return w
}

func (e *m1Env) list(t *testing.T, status string) *types.WatchListData {
	t.Helper()
	data, err := NewListWatchesLogic(e.ctx, e.svcCtx).ListWatches(&types.ListWatchesReq{
		Page:     1,
		PageSize: 50,
		Status:   status,
	})
	if err != nil {
		t.Fatalf("list watches (status=%q): %v", status, err)
	}
	return data
}

// sweepCandidates 是每日排程真正會巡的集合；RW-02／RW-04 的重點就是它有沒有變。
func (e *m1Env) sweepCandidates(t *testing.T) []string {
	t.Helper()
	list, err := e.svcCtx.Radar.ListActiveWatches(e.ctx, e.uid)
	if err != nil {
		t.Fatalf("list active watches: %v", err)
	}
	ids := make([]string, 0, len(list))
	for _, w := range list {
		ids = append(ids, w.ID)
	}
	return ids
}

func TestM1_SP01_ActiveWatchWithoutServiceProfileIsAllowed(t *testing.T) {
	env := newM1Env(t, 42, 5)

	w, err := NewCreateWatchLogic(env.ctx, env.svcCtx).CreateWatch(&types.CreateWatchReq{
		Terms:   []string{"婚攝 推薦"},
		Enabled: true,
	})
	if err != nil {
		t.Fatalf("SP-01: active watch without profile: %v", err)
	}
	if w.Status != "active" {
		t.Fatalf("SP-01: status = %q, want active", w.Status)
	}
	if got := env.list(t, ""); got.Pagination.Total != 1 || got.ActiveCount != 1 {
		t.Fatalf("SP-01: list = %+v, want one active watch", got)
	}
}

// SP-02：填完服務檔案後 GET 要拿回全部欄位；forbidden[] 是回覆生成的硬性過濾詞，尤其不能掉。
func TestM1_SP02_ServiceProfileReadsBackEveryField(t *testing.T) {
	env := newM1Env(t, 42, 5)
	env.seedProfile(t)

	got, err := NewGetServiceProfileLogic(env.ctx, env.svcCtx).GetServiceProfile()
	if err != nil {
		t.Fatalf("SP-02: get service profile: %v", err)
	}
	if !got.Exists {
		t.Fatal("SP-02: exists = false right after saving")
	}
	if len(got.Services) != 1 || got.Services[0].Name != "婚禮攝影" ||
		got.Services[0].PriceMin != 18000 || got.Services[0].PriceMax != 36000 {
		t.Fatalf("SP-02: services = %+v", got.Services)
	}
	if len(got.Forbidden) != 1 || got.Forbidden[0] != "保證接到案" {
		t.Fatalf("SP-02: forbidden = %v, want the saved words readable", got.Forbidden)
	}
	if len(got.Cases) != 1 || len(got.Faq) != 1 || len(got.ServiceAreas) != 1 {
		t.Fatalf("SP-02: cases/faq/areas lost: %+v", got)
	}
	if got.Availability != "平日全天" || got.ToneNote != "親切、不推銷" {
		t.Fatalf("SP-02: free-text fields lost: %+v", got)
	}
	if got.UpdatedAt <= 0 {
		t.Fatalf("SP-02: updated_at = %d, want unix nanoseconds", got.UpdatedAt)
	}
}

// RW-01：上限 1 的方案已有一個 active，再建一個要被擋，訊息要同時說出上限與升級路徑。
func TestM1_RW01_SecondActiveWatchOverQuotaIsRejected(t *testing.T) {
	env := newM1Env(t, 42, 1)
	env.seedProfile(t)
	first := env.createWatch(t, "婚攝 推薦", true)

	_, err := NewCreateWatchLogic(env.ctx, env.svcCtx).CreateWatch(&types.CreateWatchReq{
		Terms:   []string{"活動紀錄"},
		Enabled: true,
	})
	if err == nil {
		t.Fatal("RW-01: second active watch accepted on a 1-watch plan")
	}
	envelope := assertStatus(t, err, http.StatusBadRequest, 400100)
	if !strings.Contains(envelope.Message, "1") || !strings.Contains(envelope.Message, "upgrade") {
		t.Fatalf("RW-01: message = %q, want the current limit plus an upgrade hint", envelope.Message)
	}

	// 擋下不影響既有那筆，也不能偷偷降級它。
	after := env.list(t, "")
	if after.ActiveCount != 1 || after.MaxActive != 1 {
		t.Fatalf("RW-01: quota fields = active %d / max %d", after.ActiveCount, after.MaxActive)
	}
	if ids := env.sweepCandidates(t); len(ids) != 1 || ids[0] != first.Id {
		t.Fatalf("RW-01: sweep candidates = %v, want only the existing watch", ids)
	}

	// 停用狀態不佔配額：使用者可以先備好，暫停舊的再啟用。
	if _, err := NewCreateWatchLogic(env.ctx, env.svcCtx).CreateWatch(&types.CreateWatchReq{
		Terms:   []string{"活動紀錄"},
		Enabled: false,
	}); err != nil {
		t.Fatalf("RW-01: paused watch should not consume the active quota: %v", err)
	}
}

// RW-02：pause 後不再排入每日巡，但資料還在，恢復後照樣回到巡的名單。
func TestM1_RW02_PausedWatchLeavesTheDailySweep(t *testing.T) {
	env := newM1Env(t, 42, 5)
	env.seedProfile(t)
	w := env.createWatch(t, "婚攝 推薦", true)

	paused, err := NewPauseWatchLogic(env.ctx, env.svcCtx).PauseWatch(&types.WatchIdReq{Id: w.Id})
	if err != nil {
		t.Fatalf("RW-02: pause: %v", err)
	}
	if paused.Status != "paused" {
		t.Fatalf("RW-02: status = %q after pause", paused.Status)
	}
	if ids := env.sweepCandidates(t); len(ids) != 0 {
		t.Fatalf("RW-02: paused watch is still scheduled for sweeps: %v", ids)
	}

	// 設定保留：暫停是「先別巡」，不是刪掉。
	still := env.list(t, "")
	if still.Pagination.Total != 1 || still.ActiveCount != 0 {
		t.Fatalf("RW-02: list = %+v, want the watch kept but inactive", still)
	}
	if len(still.List[0].Terms) != 1 || still.List[0].Terms[0] != "婚攝 推薦" {
		t.Fatalf("RW-02: terms lost on pause: %+v", still.List[0])
	}

	resumed, err := NewResumeWatchLogic(env.ctx, env.svcCtx).ResumeWatch(&types.WatchIdReq{Id: w.Id})
	if err != nil || resumed.Status != "active" {
		t.Fatalf("RW-02: resume = %+v, err = %v", resumed, err)
	}
	if ids := env.sweepCandidates(t); len(ids) != 1 || ids[0] != w.Id {
		t.Fatalf("RW-02: resumed watch missing from sweeps: %v", ids)
	}
}

// RW-03：有服務檔案就給得出帶理由的建議，而且只是建議 —— 不會自己建立訂閱。
func TestM1_RW03_SuggestReturnsAdoptableTerms(t *testing.T) {
	env := newM1Env(t, 42, 5)
	env.seedProfile(t)

	data, err := NewSuggestWatchTermsLogic(env.ctx, env.svcCtx).SuggestWatchTerms(&types.SuggestWatchTermsReq{})
	if err != nil {
		t.Fatalf("RW-03: suggest: %v", err)
	}
	if len(data.List) != 3 {
		t.Fatalf("RW-03: got %d suggestions, want 3", len(data.List))
	}
	for _, s := range data.List {
		if strings.TrimSpace(s.Term) == "" || strings.TrimSpace(s.Reason) == "" {
			t.Fatalf("RW-03: suggestion without term or reason: %+v", s)
		}
		if s.Usage != "include" && s.Usage != "exclude" {
			t.Fatalf("RW-03: suggestion %q has usage %q", s.Term, s.Usage)
		}
	}
	// prompt 必須帶服務檔案，否則建議跟這個人的生意無關。
	if !strings.Contains(env.ai.prompt, "婚禮攝影") {
		t.Fatal("RW-03: prompt did not include the member's services")
	}
	if env.ai.calls != 1 {
		t.Fatalf("RW-03: AI called %d times for one suggest request", env.ai.calls)
	}

	// 建議不建立任何 watch：採用與否是使用者的決定。
	if got := env.list(t, ""); got.Pagination.Total != 0 {
		t.Fatalf("RW-03: suggest created %d watches", got.Pagination.Total)
	}

	// 逐條採用＝把建議當成 create 的輸入，這條路徑要真的走得通。
	adopted, err := NewCreateWatchLogic(env.ctx, env.svcCtx).CreateWatch(&types.CreateWatchReq{
		Terms:        []string{data.List[0].Term},
		ExcludeTerms: []string{data.List[2].Term},
		Enabled:      true,
	})
	if err != nil {
		t.Fatalf("RW-03: adopting a suggestion failed: %v", err)
	}
	if len(adopted.Terms) != 1 || len(adopted.ExcludeTerms) != 1 {
		t.Fatalf("RW-03: adopted watch = %+v", adopted)
	}

	// 計費走既有 ai_copy，source 標 radar.suggest（spec §5.5）。
	events, err := env.usage.ListEvents(env.ctx, env.uid, usageDomain.CurrentMonthKey(), "all", 0)
	if err != nil {
		t.Fatalf("RW-03: list usage events: %v", err)
	}
	if len(events) != 1 || events[0].Meter != usageDomain.MeterAICopy || events[0].Source != "radar.suggest" {
		t.Fatalf("RW-03: usage events = %+v, want one ai_copy/radar.suggest", events)
	}
}

// RW-04：封存是終點 —— 不再巡、不能恢復，但歷史還看得到。
func TestM1_RW04_ArchivedWatchStopsProducingAndStaysVisible(t *testing.T) {
	env := newM1Env(t, 42, 5)
	env.seedProfile(t)
	keep := env.createWatch(t, "婚攝 推薦", true)
	drop := env.createWatch(t, "活動紀錄", true)

	if _, err := NewArchiveWatchLogic(env.ctx, env.svcCtx).ArchiveWatch(&types.WatchIdReq{Id: drop.Id}); err != nil {
		t.Fatalf("RW-04: archive: %v", err)
	}

	if ids := env.sweepCandidates(t); len(ids) != 1 || ids[0] != keep.Id {
		t.Fatalf("RW-04: sweep candidates = %v, want only the surviving watch", ids)
	}

	// 軟刪：列表仍看得到歷史，只是狀態是 archived。
	all := env.list(t, "")
	if all.Pagination.Total != 2 || all.ActiveCount != 1 {
		t.Fatalf("RW-04: list = total %d / active %d, want 2 / 1", all.Pagination.Total, all.ActiveCount)
	}
	archived := env.list(t, "archived")
	if archived.Pagination.Total != 1 || archived.List[0].Id != drop.Id {
		t.Fatalf("RW-04: archived filter = %+v", archived.List)
	}

	// 不可復活：否則配額與統計都會出現無法解釋的跳動。
	if _, err := NewResumeWatchLogic(env.ctx, env.svcCtx).ResumeWatch(&types.WatchIdReq{Id: drop.Id}); err == nil {
		t.Fatal("RW-04: archived watch was resumed")
	} else {
		assertStatus(t, err, http.StatusBadRequest, 400100)
	}
	if _, err := NewPauseWatchLogic(env.ctx, env.svcCtx).PauseWatch(&types.WatchIdReq{Id: drop.Id}); err == nil {
		t.Fatal("RW-04: archived watch accepted a pause")
	}
}
