package usecase

import (
	"context"
	"testing"

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

func TestSearchQualityFixturesUseOneHardRelevanceGate(t *testing.T) {
	for _, fixture := range loadQualityFixtures(t) {
		t.Run(fixture.ID, func(t *testing.T) {
			for _, candidate := range fixture.Cases {
				evaluator := NewCandidateEvaluator(&domain.RunBrief{
					Mode: domain.ModeActivity, Intent: fixture.Intent, ScanTerms: fixture.Terms,
				})
				got := evaluator.Evaluate(ThreadSearchResult{URL: candidate.URL, Snippet: candidate.Text})
				if got.Decision == CandidateEligible != candidate.Relevant {
					t.Fatalf("candidate=%q decision=%s reason=%q want eligible=%v", candidate.Text, got.Decision, got.Reason, candidate.Relevant)
				}
				if candidate.Relevant && got.Reason == "" {
					t.Fatal("eligible candidate has no match reason")
				}
			}
		})
	}
}

func TestSearchQualityShortfallIsExplainableAfterAllStages(t *testing.T) {
	provider := &irrelevantPipelineProvider{}
	svc := newPipelineService(provider)
	result, err := svc.searchEligiblePipeline(context.Background(), 7, []string{"市集"}, &domain.RunBrief{
		Mode: domain.ModeActivity, Intent: "市集", ScanTerms: []string{"市集"},
	}, 8, domain.PathAPI, "", 20)
	if err != nil {
		t.Fatal(err)
	}
	if result.Diagnostics.EligibleCount != 0 || result.Diagnostics.RawCount != 120 || result.Diagnostics.Stages != 3 {
		t.Fatalf("shortfall diagnostics=%+v", result.Diagnostics)
	}
	if !containsString(result.Diagnostics.ShortfallReasons, domain.ShortfallRelevanceExhausted) {
		t.Fatalf("shortfall reasons=%v", result.Diagnostics.ShortfallReasons)
	}
}

type irrelevantPipelineProvider struct{}

func (*irrelevantPipelineProvider) SearchThreads(_ context.Context, _ []string, limit int) ([]ThreadSearchResult, error) {
	out := make([]ThreadSearchResult, 0, limit)
	for i := 0; i < limit; i++ {
		out = append(out, ThreadSearchResult{URL: "https://threads.net/@noise/post/" + itoaASCII(i), Snippet: "完全無關的貼文"})
	}
	return out, nil
}

func TestSearchQualityHistoricalDedupKeepsOldRunUntouched(t *testing.T) {
	ctx := context.Background()
	store := repository.NewMemory()
	old := domain.NewRun("old-run", "old-job", 7, domain.RunBrief{Intent: "咖啡店", Mode: domain.ModeActivity}, 10)
	old.Status = domain.RunSucceeded
	if err := store.CreateRun(ctx, old); err != nil {
		t.Fatal(err)
	}
	oldPost := &domain.Post{ID: "old-post", RunID: old.ID, OwnerUID: 7, ExternalID: "https://www.threads.net/@a/post/Same", Permalink: "https://www.threads.net/@a/post/Same", Text: "舊結果", PostedAt: 100, CreatedAt: 100}
	if err := store.PublishRunPosts(ctx, 7, old.ID, []*domain.Post{oldPost}); err != nil {
		t.Fatal(err)
	}
	if err := store.MarkSeenIdentity(ctx, 7, "threads-post:Same", oldPost.ID, oldPost.CreatedAt); err != nil {
		t.Fatal(err)
	}
	evaluator := NewCandidateEvaluator(&domain.RunBrief{Mode: domain.ModeActivity, Intent: "咖啡店", ScanTerms: []string{"咖啡店"}})
	evaluator.SetHistoricalSeenLookup(func(identity string) (bool, error) { return store.HasSeenIdentity(ctx, 7, identity) })
	got := evaluator.Evaluate(ThreadSearchResult{URL: "https://threads.com/@renamed/post/Same?x=1", Snippet: "分享咖啡店心得"})
	if got.Decision != CandidateDuplicate {
		t.Fatalf("historical duplicate=%+v", got)
	}
	kept, err := store.GetRun(ctx, 7, old.ID)
	if err != nil || kept.Status != domain.RunSucceeded {
		t.Fatalf("old run changed: %+v %v", kept, err)
	}
}

func TestSearchQualityResultsUseScoreThenPostedTimeAcrossPages(t *testing.T) {
	ctx := context.Background()
	store := repository.NewMemory()
	run := domain.NewRun("quality-order", "job-order", 7, domain.RunBrief{Intent: "市集", Mode: domain.ModeActivity}, 1)
	if err := store.CreateRun(ctx, run); err != nil {
		t.Fatal(err)
	}
	if err := run.Transition(domain.RunRunning, 2); err != nil {
		t.Fatal(err)
	}
	if err := store.ReplaceRunGuarded(ctx, 7, run.ID, []string{domain.RunQueued}, run); err != nil {
		t.Fatal(err)
	}
	svc := New(store)
	posts := []*domain.Post{
		{ID: "unknown", RunID: run.ID, OwnerUID: 7, Score: 80, PostedAt: 0, CreatedAt: 500},
		{ID: "old", RunID: run.ID, OwnerUID: 7, Score: 90, PostedAt: 100, CreatedAt: 100},
		{ID: "new", RunID: run.ID, OwnerUID: 7, Score: 100, PostedAt: 300, CreatedAt: 300},
		{ID: "same-b", RunID: run.ID, OwnerUID: 7, Score: 70, PostedAt: 200, CreatedAt: 200},
		{ID: "same-a", RunID: run.ID, OwnerUID: 7, Score: 70, PostedAt: 200, CreatedAt: 100},
	}
	if err := svc.PublishRun(ctx, 7, run.ID, posts); err != nil {
		t.Fatal(err)
	}
	page1, err := store.ListRunPosts(ctx, 7, run.ID, 1, 2)
	if err != nil || len(page1.Items) != 2 || page1.Items[0].ID != "new" || page1.Items[1].ID != "old" {
		t.Fatalf("page1=%+v %v", page1, err)
	}
	page2, err := store.ListRunPosts(ctx, 7, run.ID, 2, 2)
	if err != nil || len(page2.Items) != 2 || page2.Items[0].ID != "unknown" || page2.Items[1].ID != "same-b" {
		t.Fatalf("page2=%+v %v", page2, err)
	}
	page3, err := store.ListRunPosts(ctx, 7, run.ID, 3, 2)
	if err != nil || len(page3.Items) != 1 || page3.Items[0].ID != "same-a" {
		t.Fatalf("page3=%+v %v", page3, err)
	}
}

func containsString(items []string, want string) bool {
	for _, item := range items {
		if item == want {
			return true
		}
	}
	return false
}
