package usecase

import (
	"testing"
	"time"

	"apps/backend/internal/module/scout/domain"
)

func TestCandidateEvaluatorHasOneDecisionAndCanonicalDuplicate(t *testing.T) {
	evaluator := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "外包 後端工程師", ScanTerms: []string{"後端 外包"},
	})
	good := ThreadSearchResult{
		URL:     "https://threads.net/@builder/post/ABC123?share=1",
		Snippet: "想找後端工程師做外包合作，有推薦的接案夥伴嗎？",
	}
	first := evaluator.EvaluateAt(good, time.Now().UnixNano())
	if first.Decision != CandidateEligible || first.Identity != "threads-post:ABC123" {
		t.Fatalf("first evaluation=%+v", first)
	}
	if first.SearchTag == "" || first.Score <= 0 || first.Reason == "" {
		t.Fatalf("eligible metadata incomplete: %+v", first)
	}
	dup := evaluator.EvaluateAt(ThreadSearchResult{
		URL: "https://www.threads.com/@renamed/post/ABC123", Snippet: good.Snippet,
	}, time.Now().UnixNano())
	if dup.Decision != CandidateDuplicate || dup.Reason == "" {
		t.Fatalf("duplicate evaluation=%+v", dup)
	}
	stats := evaluator.Stats()
	if stats.Searched != 2 || stats.Eligible != 1 || stats.Duplicate != 1 || stats.Irrelevant != 0 {
		t.Fatalf("stats=%+v", stats)
	}
}

func TestCandidateEvaluatorSeparatesIrrelevanceReasons(t *testing.T) {
	evaluator := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "鬼滅之刃", ScanTerms: []string{"鬼滅之刃 最新"},
	})
	for _, tc := range []struct {
		name string
		hit  ThreadSearchResult
		want string
	}{
		{name: "missing topic", hit: ThreadSearchResult{URL: "https://threads.net/@a/post/missing", Snippet: "最近想看動畫，有推薦熱門作品嗎"}, want: "missing core"},
		{name: "noise", hit: ThreadSearchResult{URL: "https://threads.net/@a/post/noise", Snippet: "鬼滅之刃抽獎，互追拿好禮"}, want: "noise signal"},
		{name: "empty", hit: ThreadSearchResult{URL: "https://threads.net/@a/post/empty", Snippet: " "}, want: "missing canonical"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got := evaluator.Evaluate(tc.hit)
			if got.Decision != CandidateIrrelevant || got.Reason == "" || !containsReason(got.Reason, tc.want) {
				t.Fatalf("evaluation=%+v want reason containing %q", got, tc.want)
			}
		})
	}
	stats := evaluator.Stats()
	if stats.Searched != 3 || stats.Irrelevant != 3 || stats.Eligible != 0 || stats.Duplicate != 0 {
		t.Fatalf("stats=%+v", stats)
	}
}

func TestCandidateEvaluatorAllowsOldPublishedTime(t *testing.T) {
	evaluator := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "鬼滅之刃", ScanTerms: []string{"鬼滅之刃"},
	})
	got := evaluator.Evaluate(ThreadSearchResult{
		URL: "https://threads.net/@a/post/old", Snippet: "鬼滅之刃舊文",
		PublishedAt: time.Now().Add(-3 * 365 * 24 * time.Hour).UnixNano(),
	})
	if got.Decision != CandidateEligible {
		t.Fatalf("old relevant candidate=%+v", got)
	}
}

func TestCandidateEvaluatorAllowsRicherRetryAfterRejectedIdentity(t *testing.T) {
	evaluator := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "外包 後端工程師", ScanTerms: []string{"後端 外包"},
	})
	url := "https://threads.net/@builder/post/RICHER"
	first := evaluator.EvaluateAt(ThreadSearchResult{URL: url, Snippet: "外包合作正在找人"}, time.Now().UnixNano())
	if first.Decision != CandidateIrrelevant {
		t.Fatalf("sparse first hit=%+v, want irrelevant", first)
	}
	second := evaluator.EvaluateAt(ThreadSearchResult{URL: url, Snippet: "想找後端工程師做外包合作，歡迎推薦接案夥伴"}, time.Now().UnixNano())
	if second.Decision != CandidateEligible {
		t.Fatalf("richer retry=%+v, want eligible", second)
	}
	third := evaluator.EvaluateAt(ThreadSearchResult{URL: url, Snippet: second.Text}, time.Now().UnixNano())
	if third.Decision != CandidateDuplicate {
		t.Fatalf("eligible identity should be terminal duplicate: %+v", third)
	}
}

func TestCandidateEvaluatorRejectsAnchorOnlyActivityBrief(t *testing.T) {
	evaluator := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "推薦 分享", ScanTerms: []string{"推薦"},
	})
	got := evaluator.Evaluate(ThreadSearchResult{
		URL: "https://threads.net/@a/post/ANCHOR", Snippet: "推薦一間好吃的餐廳給大家",
	})
	if got.Decision != CandidateIrrelevant || got.Reason != "no approved topic core matched" {
		t.Fatalf("anchor-only activity=%+v", got)
	}
}

func TestCandidateEvaluatorUsesTitleWhenProviderOmitsSnippet(t *testing.T) {
	evaluator := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "市集", ScanTerms: []string{"市集"},
	})
	got := evaluator.Evaluate(ThreadSearchResult{
		URL: "https://threads.net/@a/post/TITLE_ONLY", Title: "台北市集週末推薦", Snippet: "",
	})
	if got.Decision != CandidateEligible {
		t.Fatalf("title-only provider hit=%+v", got)
	}
}

func TestCandidateEvaluatorAcceptsNaturalActivityIntentFromApprovedVariants(t *testing.T) {
	brief := &domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "找後端工程師接案",
		ScanTerms: []string{"後端 外包", "後端 接案", "後端 工程師"},
	}
	evaluator := NewCandidateEvaluator(brief)
	got := evaluator.Evaluate(ThreadSearchResult{
		URL:          "https://threads.net/@a/post/NATURAL_WORK",
		Snippet:      "正在找後端工程師做外包合作，歡迎推薦接案夥伴",
		MatchedQuery: "後端 外包",
	})
	if got.Decision != CandidateEligible {
		t.Fatalf("natural activity candidate=%+v", got)
	}
	// Unchecking variants must not discard the explicit role/work concepts
	// carried by the original sentence.
	oneTerm := NewCandidateEvaluator(&domain.RunBrief{
		Mode: domain.ModeActivity, Intent: brief.Intent, ScanTerms: []string{"後端 外包"},
	})
	if got := oneTerm.Evaluate(ThreadSearchResult{
		URL: "https://threads.net/@a/post/NATURAL_WORK_ONE", Snippet: "正在找後端工程師做外包合作",
	}); got.Decision != CandidateEligible {
		t.Fatalf("single approved term lost natural concepts: %+v", got)
	}
}

func containsReason(reason, want string) bool {
	for _, part := range []string{reason} {
		if len(part) >= len(want) && hasSubstring(part, want) {
			return true
		}
	}
	return false
}

func hasSubstring(text, want string) bool {
	for i := 0; i+len(want) <= len(text); i++ {
		if text[i:i+len(want)] == want {
			return true
		}
	}
	return false
}
