package usecase

import (
	"context"
	"testing"

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

type stubRadarOpps struct{ opps []*radarDomain.Opportunity }

func (s stubRadarOpps) GetOpportunity(_ context.Context, id string) (*radarDomain.Opportunity, error) {
	for _, o := range s.opps {
		if o.ID == id {
			return o, nil
		}
	}
	return nil, radarDomain.ErrNotFound
}

func (s stubRadarOpps) ListOpportunities(
	_ context.Context, ownerUID int64, f radarDomain.OpportunityListFilter,
) ([]*radarDomain.Opportunity, int64, error) {
	matched := make([]*radarDomain.Opportunity, 0, len(s.opps))
	for _, o := range s.opps {
		if o.OwnerUID != ownerUID || o.Status != radarDomain.OppAccepted {
			continue
		}
		if f.CreatedFrom > 0 && o.CreatedAt < f.CreatedFrom {
			continue
		}
		if f.CreatedTo > 0 && o.CreatedAt > f.CreatedTo {
			continue
		}
		matched = append(matched, o)
	}
	return matched, int64(len(matched)), nil
}

func seedStatsContact(t *testing.T, repo domain.Repository, handle, stage string, at int64) *domain.Contact {
	t.Helper()
	c, err := repo.UpsertContactByIdentity(context.Background(), &domain.Contact{
		OwnerUID: 7, AuthorHandle: handle, Stage: stage, LastTouchAt: at,
	})
	if err != nil {
		t.Fatal(err)
	}
	c.Stage = stage
	c.LastTouchAt = at
	if err := repo.SaveContact(context.Background(), c); err != nil {
		t.Fatal(err)
	}
	return c
}

func acceptedOpp(id, contactID, source string, createdAt int64, terms ...string) *radarDomain.Opportunity {
	return &radarDomain.Opportunity{
		ID: id, OwnerUID: 7, Status: radarDomain.OppAccepted, Source: source,
		ContactID: contactID, MatchedTerms: terms, CreatedAt: createdAt,
	}
}

func TestStatsAttributesTermsAndSources(t *testing.T) {
	ctx := context.Background()
	repo := repository.NewMemory()
	svc := New(repo)

	won := seedStatsContact(t, repo, "won-buyer", domain.StageWon, 100)
	replied := seedStatsContact(t, repo, "replied-buyer", domain.StageReplied, 100)
	cold := seedStatsContact(t, repo, "cold-buyer", domain.StageNewFound, 100)

	svc.RadarOpps = stubRadarOpps{opps: []*radarDomain.Opportunity{
		acceptedOpp("o1", won.ID, radarDomain.OppSourceThreads, 100, "婚攝"),
		// 同一聯絡人同一關鍵字的第二筆商機不可重複計入 accepted
		acceptedOpp("o2", won.ID, radarDomain.OppSourceThreads, 101, "婚攝"),
		acceptedOpp("o3", replied.ID, radarDomain.OppSourceThreads, 100, "婚攝"),
		acceptedOpp("o4", cold.ID, radarDomain.OppSourceScoutPromote, 100, "外包"),
	}}

	got, err := svc.Stats(ctx, 7, 0, 0)
	if err != nil {
		t.Fatal(err)
	}

	terms := map[string]ConversionStat{}
	for _, s := range got.Terms {
		terms[s.Key] = s
	}
	if s := terms["婚攝"]; s.Accepted != 2 || s.Replied != 2 || s.Won != 1 {
		t.Fatalf("婚攝 = %+v, want accepted 2 / replied 2 / won 1", s)
	}
	if s := terms["外包"]; s.Accepted != 1 || s.Replied != 0 || s.Won != 0 {
		t.Fatalf("外包 = %+v", s)
	}

	sources := map[string]ConversionStat{}
	for _, s := range got.Sources {
		sources[s.Key] = s
	}
	if s := sources["radar"]; s.Accepted != 2 || s.Won != 1 {
		t.Fatalf("radar source = %+v", s)
	}
	if s := sources["scout"]; s.Accepted != 1 {
		t.Fatalf("scout source = %+v", s)
	}
}

// 回覆版本維度沒有資料模型支援，必須明說不可用而不是回空清單。
func TestStatsFlagsVariantsUnavailable(t *testing.T) {
	svc := New(repository.NewMemory())
	svc.RadarOpps = stubRadarOpps{}
	got, err := svc.Stats(context.Background(), 7, 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	if !hasDimension(got.UnavailableDimensions, DimensionVariants) {
		t.Fatalf("variants must be reported unavailable, got %v", got.UnavailableDimensions)
	}
	if hasDimension(got.UnavailableDimensions, DimensionTerms) {
		t.Fatalf("terms is computable when radar is wired, got %v", got.UnavailableDimensions)
	}
}

func TestStatsFlagsTermsUnavailableWithoutRadar(t *testing.T) {
	svc := New(repository.NewMemory())
	got, err := svc.Stats(context.Background(), 7, 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	for _, dim := range []string{DimensionVariants, DimensionTerms, DimensionSources} {
		if !hasDimension(got.UnavailableDimensions, dim) {
			t.Fatalf("%s must be unavailable without radar, got %v", dim, got.UnavailableDimensions)
		}
	}
}

func TestStatsHonoursDateRange(t *testing.T) {
	ctx := context.Background()
	repo := repository.NewMemory()
	svc := New(repo)
	old := seedStatsContact(t, repo, "old-buyer", domain.StageWon, 50)
	recent := seedStatsContact(t, repo, "recent-buyer", domain.StageWon, 500)
	svc.RadarOpps = stubRadarOpps{opps: []*radarDomain.Opportunity{
		acceptedOpp("o1", old.ID, radarDomain.OppSourceThreads, 50, "舊詞"),
		acceptedOpp("o2", recent.ID, radarDomain.OppSourceThreads, 500, "新詞"),
	}}

	got, err := svc.Stats(ctx, 7, 400, 600)
	if err != nil {
		t.Fatal(err)
	}
	if got.TotalContacts != 1 || got.ByStage[domain.StageWon] != 1 {
		t.Fatalf("range should keep only the recent contact, got %+v", got)
	}
	if len(got.Terms) != 1 || got.Terms[0].Key != "新詞" {
		t.Fatalf("range should keep only the recent term, got %+v", got.Terms)
	}
}

func hasDimension(list []string, want string) bool {
	for _, v := range list {
		if v == want {
			return true
		}
	}
	return false
}
