package voucher import ( "errors" "testing" ) func TestParseRules_SimpleSku(t *testing.T) { rs, err := ParseRules("sku=ABC123|XYZ999|def456") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(rs.Conditions) != 1 { t.Fatalf("expected 1 condition got %d", len(rs.Conditions)) } c := rs.Conditions[0] if c.Kind != RuleSku { t.Fatalf("expected kind sku got %s", c.Kind) } if len(c.StringVals) != 3 { t.Fatalf("expected 3 sku values got %d", len(c.StringVals)) } want := []string{"ABC123", "XYZ999", "def456"} for i, v := range want { if c.StringVals[i] != v { t.Fatalf("expected sku[%d]=%s got %s", i, v, c.StringVals[i]) } } } func TestParseRules_CategoryAndSkuMixedSeparators(t *testing.T) { rs, err := ParseRules(" category=Shoes|Bags ; sku= A | B , min_total>=1000\nmin_item_price>=500") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(rs.Conditions) != 4 { t.Fatalf("expected 4 conditions got %d", len(rs.Conditions)) } kinds := []RuleKind{RuleCategory, RuleSku, RuleMinTotal, RuleMinItemPrice} for i, k := range kinds { if rs.Conditions[i].Kind != k { t.Fatalf("expected condition[%d] kind %s got %s", i, k, rs.Conditions[i].Kind) } } // Validate numeric thresholds if rs.Conditions[2].MinValue == nil || *rs.Conditions[2].MinValue != 1000 { t.Fatalf("expected min_total>=1000 got %+v", rs.Conditions[2]) } if rs.Conditions[3].MinValue == nil || *rs.Conditions[3].MinValue != 500 { t.Fatalf("expected min_item_price>=500 got %+v", rs.Conditions[3]) } } func TestParseRules_Empty(t *testing.T) { _, err := ParseRules(" \n ") if !errors.Is(err, ErrEmptyExpression) { t.Fatalf("expected ErrEmptyExpression got %v", err) } } func TestParseRules_Invalid(t *testing.T) { _, err := ParseRules("unknown=foo") if err == nil { t.Fatal("expected error for unknown key") } _, err = ParseRules("min_total>100") // wrong operator if err == nil { t.Fatal("expected error for wrong operator") } _, err = ParseRules("min_total>=") // missing value if err == nil { t.Fatal("expected error for missing numeric value") } } func TestRuleSet_Applies(t *testing.T) { rs := MustParseRules("sku=ABC123|XYZ999; category=Shoes|min_total>=10000; min_item_price>=3000") ctx := EvalContext{ Items: []Item{ {Sku: "ABC123", Category: "Shoes", UnitPrice: 2500}, {Sku: "FFF000", Category: "Accessories", UnitPrice: 3200}, }, CartTotalInc: 12000, } if !rs.Applies(ctx) { t.Fatalf("expected rules to apply") } // Fail due to missing sku/category ctx2 := EvalContext{ Items: []Item{ {Sku: "NOPE", Category: "Different", UnitPrice: 4000}, }, CartTotalInc: 20000, } if rs.Applies(ctx2) { t.Fatalf("expected rules NOT to apply (sku/category mismatch)") } // Fail due to min_total ctx3 := EvalContext{ Items: []Item{ {Sku: "ABC123", Category: "Shoes", UnitPrice: 2500}, {Sku: "FFF000", Category: "Accessories", UnitPrice: 3200}, }, CartTotalInc: 9000, } if rs.Applies(ctx3) { t.Fatalf("expected rules NOT to apply (min_total not reached)") } // Fail due to min_item_price (no item >=3000) ctx4 := EvalContext{ Items: []Item{ {Sku: "ABC123", Category: "Shoes", UnitPrice: 2500}, {Sku: "FFF000", Category: "Accessories", UnitPrice: 2800}, }, CartTotalInc: 15000, } if rs.Applies(ctx4) { t.Fatalf("expected rules NOT to apply (min_item_price not satisfied)") } } func TestRuleSet_Applies_CaseInsensitive(t *testing.T) { rs := MustParseRules("SKU=abc123|xyz999; CATEGORY=Shoes") ctx := EvalContext{ Items: []Item{ {Sku: "AbC123", Category: "shoes", UnitPrice: 1000}, }, CartTotalInc: 1000, } if !rs.Applies(ctx) { t.Fatalf("expected rules to apply (case-insensitive match)") } } func TestDescribe(t *testing.T) { rs := MustParseRules("sku=A|B|min_total>=500") desc := rs.Describe() // Loose assertions to avoid over-specification if desc == "" { t.Fatalf("expected non-empty description") } if !(contains(desc, "sku") && contains(desc, "min_total")) { t.Fatalf("description missing expected parts: %s", desc) } } func contains(haystack, needle string) bool { return len(haystack) >= len(needle) && indexOf(haystack, needle) >= 0 } // Simple substring search (avoid importing strings to show intent explicitly here) func indexOf(s, sub string) int { outer: for i := 0; i+len(sub) <= len(s); i++ { for j := 0; j < len(sub); j++ { if s[i+j] != sub[j] { continue outer } } return i } return -1 } func TestMustParseRules_Panics(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatalf("expected panic for invalid expression") } }() MustParseRules("~~ totally invalid ~~") }