package radar

import (
	"context"
	"testing"
	"time"

	radarDomain "apps/backend/internal/module/radar/domain"
	radarRepo "apps/backend/internal/module/radar/repository"
	radarUC "apps/backend/internal/module/radar/usecase"
)

// p0ProductSource is the same read-only boundary used by the real Scout→Radar
// bridge. Keeping it local makes this gate deterministic and network-free.
type p0ProductSource struct {
	brand    *radarUC.ProductBrand
	products map[string]*radarUC.ProductCatalogProduct
}

func (s *p0ProductSource) GetBrand(_ context.Context, ownerUID int64, id string) (*radarUC.ProductBrand, error) {
	if s.brand == nil || s.brand.ID != id {
		return nil, radarDomain.ErrNotFound
	}
	if s.brand.OwnerUID != ownerUID {
		return nil, radarDomain.ErrForbidden
	}
	return s.brand, nil
}

func (s *p0ProductSource) GetProduct(_ context.Context, ownerUID int64, id string) (*radarUC.ProductCatalogProduct, error) {
	p, ok := s.products[id]
	if !ok {
		return nil, radarDomain.ErrNotFound
	}
	if p.OwnerUID != ownerUID {
		return nil, radarDomain.ErrForbidden
	}
	return p, nil
}

type p0ContactBinder struct{ calls int }

func (b *p0ContactBinder) BindOpportunity(_ context.Context, _ int64, o *radarDomain.Opportunity) (string, error) {
	b.calls++
	return "contact:" + o.AuthorHandle, nil
}

func p0Reasons() []radarDomain.OpportunityReason {
	return []radarDomain.OpportunityReason{
		{Dimension: radarDomain.DimAuthenticity, Score: 30, Reason: "公開貼文是使用者求助"},
		{Dimension: radarDomain.DimIntent, Score: 30, Reason: "明確詢問推薦與價格"},
		{Dimension: radarDomain.DimRegion, Score: 10, Reason: "未猜測地區"},
		{Dimension: radarDomain.DimFreshness, Score: 15, Reason: "剛發布"},
		{Dimension: radarDomain.DimFit, Score: 5, Reason: "產品上下文仍保留"},
	}
}

func TestBrandProductP0FlowCreateSweepMergePrimaryQueryAndPause(t *testing.T) {
	const owner = int64(42)
	ctx := context.Background()
	now := time.Now().UnixNano()
	source := &p0ProductSource{
		brand: &radarUC.ProductBrand{ID: "b1", OwnerUID: owner, DisplayName: "澄光品牌", TargetAudience: "敏感肌使用者", UpdatedAt: 10},
		products: map[string]*radarUC.ProductCatalogProduct{
			"p1": {ID: "p1", BrandID: "b1", OwnerUID: owner, Label: "舒緩精華", ProductContext: "日常修護", PainPoints: []string{"泛紅不適"}, MatchTags: []string{"舒緩保濕"}, ProviderCapabilityTerms: []string{"修護"}, UpdatedAt: 11},
			"p2": {ID: "p2", BrandID: "b1", OwnerUID: owner, Label: "修護乳霜", ProductContext: "日常修護", PainPoints: []string{"泛紅不適"}, MatchTags: []string{"舒緩保濕"}, ProviderCapabilityTerms: []string{"修護"}, UpdatedAt: 12},
		},
	}
	svc := radarUC.New(radarRepo.NewMemory())
	svc.ProductSource = source
	svc.Quota = radarUC.FixedQuota{MaxActiveWatches: 5, MaxDailyOpportunities: 20}
	svc.CRM = &p0ContactBinder{}
	svc.HitFetch = radarUC.HitFetcherFunc(func(context.Context, int64, []string, int) ([]radarUC.ThreadHit, string, error) {
		return []radarUC.ThreadHit{{
			URL:         "https://www.threads.net/@seeker/post/shared-1",
			Snippet:     "敏感肌使用者求推薦：泛紅不適，想了解舒緩保濕日常修護與修護，請問價格？",
			PublishedAt: now,
		}}, "crawler", nil
	})

	w1, err := svc.CreateWatch(ctx, owner, radarUC.WatchInput{Terms: []string{"敏感肌"}, Enabled: true, BrandID: "b1", ProductID: "p1"})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := svc.RunSweep(ctx, owner, w1.ID, "p0-job-p1"); err != nil {
		t.Fatal(err)
	}
	w2, err := svc.CreateWatch(ctx, owner, radarUC.WatchInput{Terms: []string{"敏感肌"}, Enabled: true, BrandID: "b1", ProductID: "p2"})
	if err != nil {
		t.Fatal(err)
	}
	second, err := svc.RunSweep(ctx, owner, w2.ID, "p0-job-p2")
	if err != nil {
		t.Fatal(err)
	}
	if second.Created != 0 || second.Sweep.MatchMergedCount != 1 {
		t.Fatalf("same post was not merged: created=%d sweep=%+v", second.Created, second.Sweep)
	}

	list, total, err := svc.ListOpportunities(ctx, owner, radarDomain.OpportunityListFilter{ProductID: "p2", MatchState: "eligible", Sort: "score"})
	if err != nil || total != 1 || len(list) != 1 || len(list[0].ProductMatches) != 2 {
		t.Fatalf("expected one opportunity with two product matches: total=%d list=%+v err=%v", total, list, err)
	}
	opportunityID := list[0].ID
	if list[0].PrimaryProductID != "p1" {
		t.Fatalf("automatic primary arbitration selected %q, want p1", list[0].PrimaryProductID)
	}
	selected, err := svc.SetPrimaryProduct(ctx, owner, opportunityID, "p1", "人工確認目前主推舒緩精華")
	if err != nil {
		t.Fatal(err)
	}
	if selected.PrimaryProductID != "p1" || !selected.PrimaryProductOverridden {
		t.Fatalf("primary override not persisted: %+v", selected)
	}

	today, err := svc.GetTodayFiltered(ctx, owner, radarDomain.OpportunityListFilter{ProductID: "p2"})
	if err != nil || today.Stats.Total != 1 || len(today.High)+len(today.Mid)+len(today.Low) != 1 {
		t.Fatalf("today product filter lost eligible match: today=%+v err=%v", today, err)
	}
	accepted, contactID, err := svc.AcceptOpportunity(ctx, owner, opportunityID)
	if err != nil || contactID != "contact:seeker" || accepted.ContactID != contactID {
		t.Fatalf("contact identity was not reused: opp=%+v contact=%q err=%v", accepted, contactID, err)
	}
	if accepted.ReviewState != radarDomain.ReviewCompleted {
		t.Fatalf("accepted opportunity should leave pending inbox: opp=%+v", accepted)
	}
	pending, pendingTotal, err := svc.ListOpportunities(ctx, owner, radarDomain.OpportunityListFilter{ReviewState: radarDomain.ReviewPending})
	if err != nil || pendingTotal != 0 || len(pending) != 0 {
		t.Fatalf("accepted opportunity remained in pending inbox: total=%d list=%+v err=%v", pendingTotal, pending, err)
	}
	completed, completedTotal, err := svc.ListOpportunities(ctx, owner, radarDomain.OpportunityListFilter{ReviewState: radarDomain.ReviewCompleted})
	if err != nil || completedTotal != 1 || len(completed) != 1 || completed[0].ID != opportunityID {
		t.Fatalf("accepted opportunity missing from completed inbox: total=%d list=%+v err=%v", completedTotal, completed, err)
	}
	if binder := svc.CRM.(*p0ContactBinder); binder.calls != 1 {
		t.Fatalf("expected one CRM contact bind, calls=%d", binder.calls)
	}

	// A legacy generic row remains queryable without inventing a product match.
	legacy := &radarDomain.Opportunity{OwnerUID: owner, ExternalID: "legacy-1", Permalink: "https://threads.net/legacy-1", Text: "舊資料", PostedAt: now, Status: radarDomain.OppQualified, IntentScore: 60, IntentBand: radarDomain.BandMid, Reasons: p0Reasons()}
	if _, err := svc.Repo.UpsertByExternalID(ctx, legacy); err != nil {
		t.Fatal(err)
	}
	legacyList, legacyTotal, err := svc.ListOpportunities(ctx, owner, radarDomain.OpportunityListFilter{MatchState: "generic"})
	if err != nil || legacyTotal != 1 || len(legacyList) != 1 || len(legacyList[0].ProductMatches) != 0 {
		t.Fatalf("legacy generic query changed: total=%d list=%+v err=%v", legacyTotal, legacyList, err)
	}

	paused, err := svc.PauseProductWatches(ctx, owner, "p1")
	if err != nil || paused != 1 {
		t.Fatalf("product delete lifecycle did not pause p1 watch: count=%d err=%v", paused, err)
	}
	pausedWatch, err := svc.GetWatch(ctx, owner, w1.ID)
	if err != nil || pausedWatch.Status != radarDomain.WatchPaused || pausedWatch.PauseReason != radarDomain.PauseReasonProductUnavailable {
		t.Fatalf("paused watch lost explicit reason: %+v err=%v", pausedWatch, err)
	}
}
