package promotions

import (
	"encoding/json"
	"slices"
	"testing"
	"time"

	"git.tornberg.me/go-cart-actor/pkg/cart"
)

// --- Helpers ---------------------------------------------------------------

func cvNum(n float64) ConditionValue {
	b, _ := json.Marshal(n)
	return ConditionValue{Raw: b}
}

func cvString(s string) ConditionValue {
	b, _ := json.Marshal(s)
	return ConditionValue{Raw: b}
}

func cvStrings(ss []string) ConditionValue {
	b, _ := json.Marshal(ss)
	return ConditionValue{Raw: b}
}

// testTracer captures trace events for assertions.
type testTracer struct {
	events []traceEvent
}

type traceEvent struct {
	event string
	data  map[string]any
}

func (t *testTracer) Trace(event string, data map[string]any) {
	t.events = append(t.events, traceEvent{event: event, data: data})
}

func (t *testTracer) HasEvent(name string) bool {
	return slices.ContainsFunc(t.events, func(e traceEvent) bool { return e.event == name })
}

func (t *testTracer) Count(name string) int {
	c := 0
	for _, e := range t.events {
		if e.event == name {
			c++
		}
	}
	return c
}

// makeCart creates a cart with given total and items (each item quantity & price IncVat).
func makeCart(totalOverride int64, items []struct {
	sku      string
	category string
	qty      int
	priceInc int64
}) *cart.CartGrain {
	g := cart.NewCartGrain(1, time.Now())
	for _, it := range items {
		p := cart.NewPriceFromIncVat(it.priceInc, 0.25)
		g.Items = append(g.Items, &cart.CartItem{
			Id:    uint32(len(g.Items) + 1),
			Sku:   it.sku,
			Price: *p,
			TotalPrice: cart.Price{
				IncVat:   p.IncVat * int64(it.qty),
				VatRates: p.VatRates,
			},
			Quantity: it.qty,
			Meta: &cart.ItemMeta{
				Category: it.category,
			},
		})
	}
	// Recalculate totals
	g.UpdateTotals()
	if totalOverride >= 0 {
		g.TotalPrice.IncVat = totalOverride
	}
	return g
}

// --- Tests -----------------------------------------------------------------

func TestEvaluateRuleBasicAND(t *testing.T) {
	g := makeCart(12000, []struct {
		sku      string
		category string
		qty      int
		priceInc int64
	}{
		{"SKU-1", "summer", 2, 3000},
		{"SKU-2", "winter", 1, 6000},
	})

	ctx := NewContextFromCart(g,
		WithCustomerSegment("vip"),
		WithOrderCount(10),
		WithCustomerLifetimeValue(1234.56),
		WithNow(time.Date(2024, 6, 10, 12, 0, 0, 0, time.UTC)),
	)

	// Conditions: cart_total >= 10000 AND item_quantity >= 3 AND customer_segment = vip
	conds := Conditions{
		ConditionGroup{
			ID:       "grp",
			Type:     "group",
			Operator: LogicAND,
			Conditions: Conditions{
				BaseCondition{
					ID:       "c_cart_total",
					Type:     CondCartTotal,
					Operator: OpGreaterOrEqual,
					Value:    cvNum(10000),
				},
				BaseCondition{
					ID:       "c_item_qty",
					Type:     CondItemQuantity,
					Operator: OpGreaterOrEqual,
					Value:    cvNum(3),
				},
				BaseCondition{
					ID:       "c_segment",
					Type:     CondCustomerSegment,
					Operator: OpEquals,
					Value:    cvString("vip"),
				},
			},
		},
	}

	rule := PromotionRule{
		ID:          "rule-AND",
		Name:        "VIP Summer",
		Description: "Test rule",
		Status:      StatusActive,
		Priority:    1,
		StartDate:   "2024-06-01",
		EndDate:     ptr("2024-06-30"),
		Conditions:  conds,
		Actions: []Action{
			{ID: "a1", Type: ActionPercentageDiscount, Value: 10.0},
		},
		UsageLimit: ptrInt(100),
		UsageCount: 5,
		CreatedAt:  time.Now().Format(time.RFC3339),
		UpdatedAt:  time.Now().Format(time.RFC3339),
		CreatedBy:  "tester",
	}

	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if !res.Applicable {
		t.Fatalf("expected rule to be applicable, failedReason=%s", res.FailedReason)
	}
	if len(res.MatchedActions) != 1 {
		t.Fatalf("expected 1 action, got %d", len(res.MatchedActions))
	}
	if !tracer.HasEvent("rule_applicable") {
		t.Errorf("expected tracing event rule_applicable")
	}
}

func TestEvaluateRuleUsageLimitExhausted(t *testing.T) {
	rule := PromotionRule{
		ID:         "limit",
		Name:       "Limit",
		Status:     StatusActive,
		Priority:   1,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: Conditions{},
		UsageLimit: ptrInt(5),
		UsageCount: 5,
	}

	ctx := &PromotionEvalContext{Now: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC)}
	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if res.Applicable {
		t.Fatalf("expected rule NOT applicable due to usage limit")
	}
	if res.FailedReason != "usage_limit_exhausted" {
		t.Fatalf("expected failedReason usage_limit_exhausted, got %s", res.FailedReason)
	}
	if !tracer.HasEvent("rule_skip_usage_limit") {
		t.Errorf("tracer missing rule_skip_usage_limit event")
	}
}

func TestEvaluateRuleDateWindow(t *testing.T) {
	// Start in future
	rule := PromotionRule{
		ID:         "date",
		Name:       "DateWindow",
		Status:     StatusActive,
		Priority:   1,
		StartDate:  "2025-01-01",
		EndDate:    ptr("2025-01-31"),
		Conditions: Conditions{},
	}
	ctx := &PromotionEvalContext{Now: time.Date(2024, 12, 15, 12, 0, 0, 0, time.UTC)}
	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if res.Applicable {
		t.Fatalf("expected rule NOT applicable (before start)")
	}
	if res.FailedReason != "before_start" {
		t.Fatalf("expected failedReason before_start, got %s", res.FailedReason)
	}
	if !tracer.HasEvent("rule_skip_before_start") {
		t.Errorf("missing rule_skip_before_start trace event")
	}
}

func TestEvaluateProductCategoryIN(t *testing.T) {
	g := makeCart(-1, []struct {
		sku      string
		category string
		qty      int
		priceInc int64
	}{
		{"A", "shoes", 1, 5000},
		{"B", "bags", 1, 7000},
	})

	ctx := NewContextFromCart(g)
	conds := Conditions{
		BaseCondition{
			ID:       "c_category",
			Type:     CondProductCategory,
			Operator: OpIn,
			Value:    cvStrings([]string{"shoes", "hats"}),
		},
	}
	rule := PromotionRule{
		ID:         "cat-in",
		Status:     StatusActive,
		Priority:   10,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: conds,
		Actions: []Action{
			{ID: "discount", Type: ActionFixedDiscount, Value: 1000},
		},
	}
	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if !res.Applicable {
		t.Fatalf("expected category IN rule to apply")
	}
	if !tracer.HasEvent("rule_applicable") {
		t.Errorf("tracing missing rule_applicable")
	}
}

func TestEvaluateGroupOR(t *testing.T) {
	g := makeCart(3000, []struct {
		sku      string
		category string
		qty      int
		priceInc int64
	}{
		{"ONE", "x", 1, 3000},
	})

	ctx := NewContextFromCart(g)

	// OR group: (cart_total >= 10000) OR (item_quantity >= 1)
	group := ConditionGroup{
		ID:       "grp-or",
		Type:     "group",
		Operator: LogicOR,
		Conditions: Conditions{
			BaseCondition{
				ID:       "c_total",
				Type:     CondCartTotal,
				Operator: OpGreaterOrEqual,
				Value:    cvNum(10000),
			},
			BaseCondition{
				ID:       "c_qty",
				Type:     CondItemQuantity,
				Operator: OpGreaterOrEqual,
				Value:    cvNum(1),
			},
		},
	}
	rule := PromotionRule{
		ID:         "or-rule",
		Status:     StatusActive,
		Priority:   5,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: Conditions{group},
		Actions:    []Action{{ID: "a", Type: ActionFixedDiscount, Value: 500}},
	}
	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if !res.Applicable {
		t.Fatalf("expected OR rule to apply (second condition true)")
	}
	// Ensure group pass event
	if !tracer.HasEvent("cond_group_or_pass") {
		t.Errorf("expected cond_group_or_pass trace event")
	}
}

func TestEvaluateAllPriorityOrdering(t *testing.T) {
	ctx := &PromotionEvalContext{
		CartTotalIncVat:   20000,
		TotalItemQuantity: 2,
		Items: []PromotionItem{
			{SKU: "X", Quantity: 2, Category: "general", PriceIncVat: 10000},
		},
		Now: time.Date(2024, 5, 10, 10, 0, 0, 0, time.UTC),
	}

	// Rule A: priority 5
	ruleA := PromotionRule{
		ID:         "A",
		Status:     StatusActive,
		Priority:   5,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: Conditions{},
		Actions: []Action{
			{ID: "actionA", Type: ActionFixedDiscount, Value: 100},
		},
	}

	// Rule B: priority 1
	ruleB := PromotionRule{
		ID:         "B",
		Status:     StatusActive,
		Priority:   1,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: Conditions{},
		Actions: []Action{
			{ID: "actionB", Type: ActionFixedDiscount, Value: 200},
		},
	}

	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	results, actions := svc.EvaluateAll([]PromotionRule{ruleA, ruleB}, ctx)
	if len(results) != 2 {
		t.Fatalf("expected 2 results, got %d", len(results))
	}
	if len(actions) != 2 {
		t.Fatalf("expected 2 actions, got %d", len(actions))
	}

	// Actions should follow priority order: ruleB (1) then ruleA (5)
	if actions[0].ID != "actionB" || actions[1].ID != "actionA" {
		t.Fatalf("actions order invalid: %+v", actions)
	}
	if tracer.Count("actions_add") != 2 {
		t.Errorf("expected 2 actions_add trace events, got %d", tracer.Count("actions_add"))
	}
	if !tracer.HasEvent("evaluation_complete") {
		t.Errorf("missing evaluation_complete trace")
	}
}

func TestDayOfWeekAndTimeOfDay(t *testing.T) {
	// Wednesday 14:30 UTC
	now := time.Date(2024, 6, 12, 14, 30, 0, 0, time.UTC) // 2024-06-12 is Wednesday
	ctx := &PromotionEvalContext{
		Now: now,
	}

	condDay := BaseCondition{
		ID:       "dow",
		Type:     CondDayOfWeek,
		Operator: OpIn,
		Value:    cvStrings([]string{"wed", "fri"}),
	}
	condTime := BaseCondition{
		ID:       "tod",
		Type:     CondTimeOfDay,
		Operator: OpEquals, // operator is ignored for time-of-day internally
		Value:    cvString("13:00-15:00"),
	}

	rule := PromotionRule{
		ID:         "day-time",
		Status:     StatusActive,
		Priority:   1,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: Conditions{condDay, condTime},
		Actions:    []Action{{ID: "a", Type: ActionPercentageDiscount, Value: 15}},
	}

	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if !res.Applicable {
		t.Fatalf("expected rule to apply for Wednesday 14:30 in window 13-15")
	}
}

func TestDateRangeCondition(t *testing.T) {
	now := time.Date(2024, 3, 15, 9, 0, 0, 0, time.UTC)
	ctx := &PromotionEvalContext{Now: now}

	condRange := BaseCondition{
		ID:       "date_range",
		Type:     CondDateRange,
		Operator: OpEquals, // not used
		Value:    cvStrings([]string{"2024-03-01", "2024-03-31"}),
	}

	rule := PromotionRule{
		ID:         "range",
		Status:     StatusActive,
		Priority:   1,
		StartDate:  "2024-01-01",
		EndDate:    nil,
		Conditions: Conditions{condRange},
		Actions:    []Action{{ID: "a", Type: ActionFixedDiscount, Value: 500}},
	}

	tracer := &testTracer{}
	svc := NewPromotionService(tracer)
	res := svc.EvaluateRule(rule, ctx)
	if !res.Applicable {
		t.Fatalf("expected date range rule to apply for 2024-03-15")
	}
}

// --- Utilities -------------------------------------------------------------

func ptr(s string) *string { return &s }
func ptrInt(i int) *int    { return &i }
