package usecase

import (
	"context"
	"errors"
	"strings"
	"testing"

	"apps/backend/internal/module/radar/domain"
	"apps/backend/internal/module/radar/repository"
)

// 大部分 watch 行為都要求服務檔案已存在，所以測試從「已建檔」起跑。
func serviceWithProfile(t *testing.T, maxActive int) (*Service, context.Context) {
	t.Helper()
	svc := New(repository.NewMemory())
	svc.Quota = FixedQuota{MaxActiveWatches: maxActive, MaxDailyOpportunities: 30}
	ctx := context.Background()
	if _, err := svc.UpsertServiceProfile(ctx, 42, sampleProfile()); err != nil {
		t.Fatalf("seed profile: %v", err)
	}
	return svc, ctx
}

func watchInput() WatchInput {
	return WatchInput{Terms: []string{"婚攝 推薦"}, Enabled: true}
}

func TestWatchTermsAreNormalized(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	w, err := svc.CreateWatch(ctx, 42, WatchInput{
		Terms:        []string{" Wedding  Photo ", "wedding photo", "婚攝　推薦"},
		ExcludeTerms: []string{"徵才", "徵才"},
		Regions:      []string{"tpe", "TPE"},
		Enabled:      true,
	})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	// 大小寫與多餘空白必須收斂，否則同一個詞會在關鍵字統計裡拆成好幾列。
	if len(w.Terms) != 2 || w.Terms[0] != "wedding photo" {
		t.Fatalf("terms = %v, want deduped lower-case", w.Terms)
	}
	if w.Terms[1] != "婚攝 推薦" {
		t.Fatalf("full-width space not collapsed: %q", w.Terms[1])
	}
	if len(w.ExcludeTerms) != 1 || len(w.Regions) != 1 || w.Regions[0] != "TPE" {
		t.Fatalf("exclude/regions not normalized: %+v", w)
	}
}

func TestCreateWatchRejectsBadInput(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	cases := map[string]WatchInput{
		"no terms":           {Terms: nil, Enabled: true},
		"blank terms":        {Terms: []string{"  ", ""}, Enabled: true},
		"term too short":     {Terms: []string{"a"}, Enabled: true},
		"unknown region":     {Terms: []string{"婚攝"}, Regions: []string{"台北"}, Enabled: true},
		"term also excluded": {Terms: []string{"婚攝"}, ExcludeTerms: []string{"婚攝"}, Enabled: true},
	}
	for name, in := range cases {
		t.Run(name, func(t *testing.T) {
			if _, err := svc.CreateWatch(ctx, 42, in); !errors.Is(err, domain.ErrValidation) {
				t.Fatalf("err = %v, want ErrValidation", err)
			}
		})
	}
}

func TestCreateWatchExpandsUnsearchableTerms(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	w, err := svc.CreateWatch(ctx, 42, WatchInput{
		Terms:   []string{"晚上睡覺容易口乾舌燥怎麼辦"},
		Enabled: true,
	})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if len(w.Terms) == 0 {
		t.Fatal("long sentence must expand into searchable terms")
	}
	for _, term := range w.Terms {
		if !domain.IsThreadsSearchable(term) {
			t.Fatalf("persisted term %q is not searchable", term)
		}
	}
}

func TestWatchPauseResumeRoundTrip(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if w.Status != domain.WatchActive {
		t.Fatalf("status = %q, want active", w.Status)
	}

	paused, err := svc.PauseWatch(ctx, 42, w.ID)
	if err != nil {
		t.Fatalf("pause: %v", err)
	}
	if paused.Status != domain.WatchPaused {
		t.Fatalf("status = %q, want paused", paused.Status)
	}
	// RW-02：暫停後不再排入每日巡。
	active, err := svc.ListActiveWatches(ctx, 42)
	if err != nil {
		t.Fatalf("list active: %v", err)
	}
	if len(active) != 0 {
		t.Fatalf("paused watch still in active list: %+v", active)
	}

	resumed, err := svc.ResumeWatch(ctx, 42, w.ID)
	if err != nil {
		t.Fatalf("resume: %v", err)
	}
	if resumed.Status != domain.WatchActive {
		t.Fatalf("status = %q, want active", resumed.Status)
	}
	if active, _ = svc.ListActiveWatches(ctx, 42); len(active) != 1 {
		t.Fatalf("resumed watch missing from active list: %+v", active)
	}
}

// RW-04：archived 是終態，歷史資料留著但不能復活。
func TestArchivedWatchIsTerminal(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := svc.ArchiveWatch(ctx, 42, w.ID); err != nil {
		t.Fatalf("archive: %v", err)
	}

	if _, err := svc.ResumeWatch(ctx, 42, w.ID); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("resume after archive: err = %v, want ErrValidation", err)
	}
	if _, err := svc.PauseWatch(ctx, 42, w.ID); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("pause after archive: err = %v, want ErrValidation", err)
	}
	terms := []string{"改個詞"}
	if _, err := svc.UpdateWatch(ctx, 42, w.ID, WatchPatch{Terms: &terms}); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("edit after archive: err = %v, want ErrValidation", err)
	}

	active, err := svc.ListActiveWatches(ctx, 42)
	if err != nil || len(active) != 0 {
		t.Fatalf("archived watch still active: %+v (%v)", active, err)
	}
	// 軟刪：列表仍看得到，統計與歷史才有依據。
	list, total, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{})
	if err != nil || total != 1 || len(list) != 1 || list[0].Status != domain.WatchArchived {
		t.Fatalf("archived watch was hard-deleted: list=%+v total=%d err=%v", list, total, err)
	}
}

func TestDeleteArchivedWatchRemovesDefinitionKeepsHistoryBoundary(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := svc.ArchiveWatch(ctx, 42, w.ID); err != nil {
		t.Fatalf("archive: %v", err)
	}
	if err := svc.DeleteArchivedWatch(ctx, 42, w.ID); err != nil {
		t.Fatalf("delete archived: %v", err)
	}

	if _, err := svc.GetWatch(ctx, 42, w.ID); !errors.Is(err, domain.ErrNotFound) {
		t.Fatalf("deleted watch lookup err = %v, want ErrNotFound", err)
	}
	list, total, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{})
	if err != nil {
		t.Fatalf("list after delete: %v", err)
	}
	if total != 0 || len(list) != 0 {
		t.Fatalf("deleted watch still listed: list=%+v total=%d", list, total)
	}
	// Sweeps/opportunities are separate repository records and are intentionally
	// not touched by deleting the watch definition.
}

func TestDeleteArchivedWatchRejectsNonArchived(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := svc.DeleteArchivedWatch(ctx, 42, w.ID); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("delete active err = %v, want ErrValidation", err)
	}
	if _, err := svc.GetWatch(ctx, 42, w.ID); err != nil {
		t.Fatalf("active watch disappeared after rejected delete: %v", err)
	}
}

func TestUpdateWatchOnlyTouchesGivenFields(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)

	w, err := svc.CreateWatch(ctx, 42, WatchInput{
		Terms:        []string{"婚攝 推薦"},
		ExcludeTerms: []string{"徵才"},
		Regions:      []string{"TPE"},
		Enabled:      true,
	})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	regions := []string{"KHH"}
	updated, err := svc.UpdateWatch(ctx, 42, w.ID, WatchPatch{Regions: &regions})
	if err != nil {
		t.Fatalf("update: %v", err)
	}
	if len(updated.Terms) != 1 || updated.Terms[0] != "婚攝 推薦" {
		t.Fatalf("terms changed by a regions-only patch: %v", updated.Terms)
	}
	if len(updated.ExcludeTerms) != 1 {
		t.Fatalf("exclude_terms changed by a regions-only patch: %v", updated.ExcludeTerms)
	}
	if len(updated.Regions) != 1 || updated.Regions[0] != "KHH" {
		t.Fatalf("regions = %v, want [KHH]", updated.Regions)
	}
	// 狀態只走 pause／resume／archive，patch 不該碰它。
	if updated.Status != domain.WatchActive {
		t.Fatalf("status = %q, want active", updated.Status)
	}
}

// 服務檔案是選項：沒建檔也能用關鍵字開巡邏。
func TestActiveWatchDoesNotRequireServiceProfile(t *testing.T) {
	svc := New(repository.NewMemory())
	svc.Quota = FixedQuota{MaxActiveWatches: 5, MaxDailyOpportunities: 30}
	ctx := context.Background()

	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create active without profile: %v", err)
	}
	if w.Status != domain.WatchActive {
		t.Fatalf("status = %q, want active", w.Status)
	}

	paused, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"婚攝 推薦"}})
	if err != nil {
		t.Fatalf("create paused without profile: %v", err)
	}
	if paused.Status != domain.WatchPaused {
		t.Fatalf("status = %q, want paused", paused.Status)
	}
	if _, err := svc.ResumeWatch(ctx, 42, paused.ID); err != nil {
		t.Fatalf("resume without profile: %v", err)
	}
}

// RW-01：Free 只有 1 個 active，第二個要被明確拒絕且訊息帶上限。
func TestActiveWatchQuotaGate(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 1)

	first, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("first create: %v", err)
	}

	_, err = svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"活動紀錄"}, Enabled: true})
	if !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("err = %v, want ErrValidation", err)
	}
	if !strings.Contains(err.Error(), "1") || !strings.Contains(err.Error(), "upgrade") {
		t.Fatalf("error must carry the limit and an upgrade hint, got %q", err)
	}

	// 暫停第一個之後就有位置了。
	if _, err := svc.PauseWatch(ctx, 42, first.ID); err != nil {
		t.Fatalf("pause: %v", err)
	}
	second, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"活動紀錄"}, Enabled: true})
	if err != nil {
		t.Fatalf("create after pause: %v", err)
	}
	// 而恢復第一個又會超限：暫停再恢復不能當成繞過上限的路。
	if _, err := svc.ResumeWatch(ctx, 42, first.ID); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("resume over quota: err = %v, want ErrValidation", err)
	}
	if second.Status != domain.WatchActive {
		t.Fatalf("second watch status = %q", second.Status)
	}
}

// 方案降級後既有超額訂閱不強制降級，只擋新增（spec §3.1）。
func TestExistingOverQuotaWatchesAreNotDowngraded(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 3)
	for _, term := range []string{"婚攝 推薦", "活動紀錄", "商品攝影"} {
		if _, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{term}, Enabled: true}); err != nil {
			t.Fatalf("create %s: %v", term, err)
		}
	}

	svc.Quota = FixedQuota{MaxActiveWatches: 1, MaxDailyOpportunities: 5}

	active, err := svc.ListActiveWatches(ctx, 42)
	if err != nil || len(active) != 3 {
		t.Fatalf("existing watches were downgraded: %d (%v)", len(active), err)
	}
	if _, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"新的詞"}, Enabled: true}); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("err = %v, want ErrValidation for a new watch over quota", err)
	}
}

func TestWatchesAreIsolatedPerOwner(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	if _, err := svc.GetWatch(ctx, 43, w.ID); !errors.Is(err, domain.ErrForbidden) {
		t.Fatalf("owner 43 read owner 42's watch: %v", err)
	}
	if _, err := svc.PauseWatch(ctx, 43, w.ID); !errors.Is(err, domain.ErrForbidden) {
		t.Fatalf("owner 43 paused owner 42's watch: %v", err)
	}
	if err := svc.ArchiveWatch(ctx, 43, w.ID); !errors.Is(err, domain.ErrForbidden) {
		t.Fatalf("owner 43 archived owner 42's watch: %v", err)
	}
	list, total, err := svc.ListWatches(ctx, 43, domain.WatchListFilter{})
	if err != nil || total != 0 || len(list) != 0 {
		t.Fatalf("owner 43 listed owner 42's watches: %+v (%d, %v)", list, total, err)
	}
}

func TestListWatchesFilterAndPaging(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	for _, term := range []string{"婚攝 推薦", "活動紀錄", "商品攝影"} {
		if _, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{term}, Enabled: true}); err != nil {
			t.Fatalf("create %s: %v", term, err)
		}
	}
	list, _, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if _, err := svc.PauseWatch(ctx, 42, list[0].ID); err != nil {
		t.Fatalf("pause: %v", err)
	}

	paused, total, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{Status: domain.WatchPaused})
	if err != nil || total != 1 || len(paused) != 1 {
		t.Fatalf("status filter: list=%+v total=%d err=%v", paused, total, err)
	}
	if _, _, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{Status: "nope"}); !errors.Is(err, domain.ErrValidation) {
		t.Fatalf("unknown status filter should be rejected, got %v", err)
	}

	page1, total, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{Page: 1, PageSize: 2})
	if err != nil || total != 3 || len(page1) != 2 {
		t.Fatalf("page 1: list=%d total=%d err=%v", len(page1), total, err)
	}
	page2, _, err := svc.ListWatches(ctx, 42, domain.WatchListFilter{Page: 2, PageSize: 2})
	if err != nil || len(page2) != 1 {
		t.Fatalf("page 2: list=%d err=%v", len(page2), err)
	}
}

func TestMarkWatchSweptAt(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := svc.MarkWatchSwept(ctx, w.ID, 0); err != nil {
		t.Fatalf("mark swept: %v", err)
	}
	got, err := svc.GetWatch(ctx, 42, w.ID)
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.LastSweptAt <= 0 {
		t.Fatalf("last_swept_at = %d, want a timestamp", got.LastSweptAt)
	}
	if err := svc.MarkWatchSwept(ctx, "missing", 0); !errors.Is(err, domain.ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

// 新用戶建第一組 active 訂閱應立即排入首巡，不用等每日排程；
// 第二組訂閱不該再重複觸發，避免每次新增都多燒一次巡檢成本。
func TestCreateWatch_FirstActiveWatchTriggersImmediateSweep(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	var scheduled []string
	svc.SweepJobs = SweepJobSchedulerFunc(func(_ context.Context, ownerUID int64, watchID string, _ int64) (string, error) {
		scheduled = append(scheduled, watchID)
		return "job-" + watchID, nil
	})

	first, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create first: %v", err)
	}
	if !first.FirstSweepTriggered {
		t.Fatalf("first watch should report FirstSweepTriggered=true")
	}
	if len(scheduled) != 1 || scheduled[0] != first.ID {
		t.Fatalf("scheduled = %v, want exactly [%s]", scheduled, first.ID)
	}

	second, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"活動紀錄"}, Enabled: true})
	if err != nil {
		t.Fatalf("create second: %v", err)
	}
	if second.FirstSweepTriggered {
		t.Fatalf("second watch should not trigger another immediate sweep")
	}
	if len(scheduled) != 1 {
		t.Fatalf("scheduled = %v, want no additional sweep for the second watch", scheduled)
	}
}

// SweepJobs 未配置（例如測試環境）時不該讓建立失敗，也不該回報已觸發首巡。
func TestCreateWatch_FirstWatchWithoutSweepJobsDoesNotFail(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	w, err := svc.CreateWatch(ctx, 42, watchInput())
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if w.FirstSweepTriggered {
		t.Fatalf("FirstSweepTriggered should be false when SweepJobs is nil")
	}
}

// 第一組訂閱先以 paused 建立時不算「第一組 active」，之後才建的 active 訂閱
// 不會觸發首巡：規則只看「使用者是否曾建過任何訂閱」，不特別追第一個 active。
func TestCreateWatch_FirstWatchPausedThenActiveSecondSkipsImmediateSweep(t *testing.T) {
	svc, ctx := serviceWithProfile(t, 5)
	var scheduled []string
	svc.SweepJobs = SweepJobSchedulerFunc(func(_ context.Context, _ int64, watchID string, _ int64) (string, error) {
		scheduled = append(scheduled, watchID)
		return "job-" + watchID, nil
	})

	if _, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"婚攝 推薦"}, Enabled: false}); err != nil {
		t.Fatalf("create paused: %v", err)
	}
	active, err := svc.CreateWatch(ctx, 42, WatchInput{Terms: []string{"活動紀錄"}, Enabled: true})
	if err != nil {
		t.Fatalf("create active: %v", err)
	}
	if active.FirstSweepTriggered || len(scheduled) != 0 {
		t.Fatalf("expected no immediate sweep after an earlier paused watch already existed, scheduled=%v", scheduled)
	}
}

func TestWatchTransitionMatrix(t *testing.T) {
	allowed := map[string][]string{
		domain.WatchActive:   {domain.WatchPaused, domain.WatchArchived},
		domain.WatchPaused:   {domain.WatchActive, domain.WatchArchived},
		domain.WatchArchived: {},
	}
	states := []string{domain.WatchActive, domain.WatchPaused, domain.WatchArchived}
	for _, from := range states {
		for _, to := range states {
			want := from == to
			for _, ok := range allowed[from] {
				if ok == to {
					want = true
				}
			}
			if got := domain.CanTransitionWatch(from, to); got != want {
				t.Fatalf("CanTransitionWatch(%s, %s) = %v, want %v", from, to, got, want)
			}
		}
	}
}
