package usecase

import (
	"context"
	"errors"
	"testing"

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

func exploreSvc(t *testing.T) (*Service, context.Context) {
	t.Helper()
	svc := New(repository.NewMemory())
	svc.Quota = FixedQuota{MaxActiveWatches: 5, MaxDailyOpportunities: 30}
	ctx := context.Background()
	if _, err := svc.UpsertServiceProfile(ctx, 7, sampleProfile()); err != nil {
		t.Fatalf("seed profile: %v", err)
	}
	return svc, ctx
}

func TestExploreRejectsBadTerms(t *testing.T) {
	svc, ctx := exploreSvc(t)
	cases := [][]string{
		nil,
		{},
		{"台北 婚攝 推薦 價格"}, // too many tokens
		{"ok", "ok2", "ok3", "ok4", "ok5", "ok6", "ok7"}, // >6
		{"a"},
	}
	for _, terms := range cases {
		_, err := svc.ExploreOpportunities(ctx, 7, terms)
		if !errors.Is(err, domain.ErrValidation) {
			t.Fatalf("terms=%v err=%v, want ErrValidation", terms, err)
		}
	}
}

func TestExploreFanOutHitsAndCreates(t *testing.T) {
	svc, ctx := exploreSvc(t)
	calls := 0
	svc.HitFetch = HitFetcherFunc(func(_ context.Context, _ int64, terms []string, limit int) ([]ThreadHit, string, error) {
		calls++
		var hits []ThreadHit
		for i, term := range terms {
			// unique URLs so each term can produce a create
			hits = append(hits, ThreadHit{
				URL:     "https://www.threads.net/@u/post/e" + string(rune('a'+i)),
				Title:   term,
				Snippet: "求推薦 " + term + "，有人知道嗎？怎麼辦",
			})
		}
		if len(hits) > limit {
			hits = hits[:limit]
		}
		return hits, domain.SweepPathAPI, nil
	})

	res, err := svc.ExploreOpportunities(ctx, 7, []string{"保母 求推薦", "到府保母"})
	if err != nil {
		t.Fatalf("explore: %v", err)
	}
	if calls != 1 {
		t.Fatalf("HitFetch calls=%d want 1", calls)
	}
	if res.HitCount != 2 {
		t.Fatalf("hit_count=%d want 2", res.HitCount)
	}
	if res.SweepID == "" {
		t.Fatal("missing sweep_id")
	}
	if res.JudgedCount < 1 {
		t.Fatalf("judged=%d want >=1", res.JudgedCount)
	}
	if res.CreatedCount < 1 {
		t.Fatalf("created=%d want >=1", res.CreatedCount)
	}
}

func TestExploreTruncatesWhenQuotaZero(t *testing.T) {
	svc, ctx := exploreSvc(t)
	max, err := svc.MaxDailyOpportunities(ctx, 7)
	if err != nil {
		t.Fatalf("max: %v", err)
	}
	now := domain.NowNano()
	for i := 0; i < max; i++ {
		id := domain.NewID()
		o := &domain.Opportunity{
			ID: id, OwnerUID: 7, Source: domain.OppSourceThreads,
			ExternalID: "https://x/" + id, Permalink: "https://x/" + id,
			AuthorHandle: "u", Text: "dummy", PostedAt: now,
			Status: domain.OppQualified, IntentScore: 80, IntentBand: domain.BandHigh,
			Reasons: sampleExploreReasons(), RegionMatch: domain.RegionUnknown, FreshnessHours: 1,
			CreatedAt: now, UpdatedAt: now,
		}
		if _, err := svc.Repo.UpsertByExternalID(ctx, o); err != nil {
			t.Fatalf("seed opp: %v", err)
		}
	}

	svc.HitFetch = HitFetcherFunc(func(_ context.Context, _ int64, _ []string, _ int) ([]ThreadHit, string, error) {
		return []ThreadHit{{
			URL: "https://www.threads.net/@u/post/new1", Title: "t",
			Snippet: "求推薦 到府保母，有人知道嗎？",
		}}, domain.SweepPathAPI, nil
	})

	res, err := svc.ExploreOpportunities(ctx, 7, []string{"保母 求推薦"})
	if err != nil {
		t.Fatalf("explore: %v", err)
	}
	if res.CreatedCount != 0 {
		t.Fatalf("created=%d want 0 when quota full", res.CreatedCount)
	}
	if res.TruncatedCount < 1 {
		t.Fatalf("truncated=%d want >=1", res.TruncatedCount)
	}
}

func TestExploreJudgeFailureTolerated(t *testing.T) {
	// Even when AI is down, heuristic judge should still work and explore must not crash.
	svc, ctx := exploreSvc(t)
	svc.HitFetch = HitFetcherFunc(func(_ context.Context, _ int64, _ []string, _ int) ([]ThreadHit, string, error) {
		return []ThreadHit{{
			URL: "https://www.threads.net/@u/post/jfail", Title: "t",
			Snippet: "求推薦 保母 有人知道嗎",
		}}, domain.SweepPathAPI, nil
	})
	svc.AI = &stubAI{err: errors.New("ai down")}

	res, err := svc.ExploreOpportunities(ctx, 7, []string{"保母 求推薦"})
	if err != nil {
		t.Fatalf("explore must tolerate AI failure via heuristic: %v", err)
	}
	if res.JudgedCount < 1 {
		t.Fatalf("judged=%d want >=1", res.JudgedCount)
	}
}

func sampleExploreReasons() []domain.OpportunityReason {
	return []domain.OpportunityReason{
		{Dimension: domain.DimAuthenticity, Score: 20, Reason: "ok"},
		{Dimension: domain.DimIntent, Score: 20, Reason: "ok"},
		{Dimension: domain.DimRegion, Score: 10, Reason: "ok"},
		{Dimension: domain.DimFreshness, Score: 15, Reason: "ok"},
		{Dimension: domain.DimFit, Score: 15, Reason: "ok"},
	}
}
