package growth import "testing" func TestCatalog1405IsValidAndDefensivelyCopied(t *testing.T) { first := Catalog1405() if err := ValidateCatalog(first); err != nil { t.Fatalf("catalog should be valid: %v", err) } first[0].Tags[0] = "mutated" second := Catalog1405() if second[0].Tags[0] == "mutated" { t.Fatal("catalog tags escaped through a shared backing array") } } func TestValidateCatalogRejectsDuplicateAndIncompleteEvents(t *testing.T) { event := officialEvent("duplicate", "رویداد", "2026-01-01", "2026-01-01", "family", false, []string{"tag"}) if err := ValidateCatalog([]Event{event, event}); err == nil { t.Fatal("expected duplicate id error") } event.Source = "" if err := ValidateCatalog([]Event{event}); err == nil { t.Fatal("expected missing source error") } } func TestCatalogCoversImportedHistoryAndForecastHorizon(t *testing.T) { catalog := Catalog1405() if catalog[0].StartDate.After(date("2026-01-03")) || catalog[len(catalog)-1].EndDate.Before(date("2026-09-28")) { t.Fatalf("catalog coverage is incomplete: %s..%s", catalog[0].StartDate, catalog[len(catalog)-1].EndDate) } var scoped bool for _, event := range catalog { if event.ID == "back-to-school-1405" { scoped = !event.Universal && len(event.Categories) == 2 } } if !scoped { t.Fatal("back-to-school event must preserve explicit category scope") } } func TestAnalyzeEventEffectExcludesCatalogEventsFromBaseline(t *testing.T) { target := Event{ID: "target", StartDate: date("2026-05-02"), EndDate: date("2026-05-02")} excluded := Event{ID: "other", StartDate: target.StartDate.AddDate(0, 0, -7), EndDate: target.StartDate.AddDate(0, 0, -7)} history := make([]DailyPoint, 0, 9) for week := 1; week <= 8; week++ { count := int64(30) if week == 1 { count = 1_000 } history = append(history, DailyPoint{Date: target.StartDate.AddDate(0, 0, -7*week), SessionCount: count, PaidSessionCount: count, PaidAmountRial: count * 100}) } history = append(history, DailyPoint{Date: target.StartDate, SessionCount: 50, PaidSessionCount: 50, PaidAmountRial: 5_000}) effect := AnalyzeEventEffect(history, target, []Event{excluded}) if effect.ObservationCount != 7 || effect.BaselineSessionCount != 30 || !effect.MaterialWin { t.Fatalf("catalog event polluted baseline: %+v", effect) } } func TestAnalyzeEventEffectUsesCompleteComparableWindows(t *testing.T) { target := Event{ID: "two-day", StartDate: date("2026-05-02"), EndDate: date("2026-05-03")} history := []DailyPoint{ {Date: date("2026-04-25"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-04-26"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-04-18"), SessionCount: 1_000, PaidSessionCount: 1_000, PaidAmountRial: 100_000}, // fragmented week, next day absent {Date: date("2026-04-11"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-04-12"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-04-04"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-04-05"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-03-28"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: date("2026-03-29"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000}, {Date: target.StartDate, SessionCount: 80, PaidSessionCount: 75, PaidAmountRial: 7_500}, {Date: target.EndDate, SessionCount: 80, PaidSessionCount: 75, PaidAmountRial: 7_500}, } effect := AnalyzeEventEffect(history, target, nil) if effect.ObservationCount != 4 || effect.BaselineSessionCount != 120 || effect.ActualSessionCount != 160 || effect.PaidSessionUpliftBPS != 5_000 { t.Fatalf("unexpected complete-window effect: %+v", effect) } } func TestForecastKeepsPoorBacktestVisibleWithLowConfidence(t *testing.T) { start := date("2026-07-01") history := make([]DailyPoint, 0, 140) for i := 140; i > 0; i-- { value := int64(100) if i%2 == 0 { value = 1_000 } history = append(history, DailyPoint{Date: start.AddDate(0, 0, -i), SessionCount: value, PaidSessionCount: value, PaidAmountRial: value * 1_000}) } forecast := BuildForecast(history, nil, start, 90) if forecast.Backtest.Origins < 4 || forecast.Confidence != ConfidenceLow || len(forecast.Points) != 90 { t.Fatalf("poor model should remain visible with low confidence: %+v", forecast) } } func TestBacktestUsesProductionTrendPredictorAcrossFourteenDayHorizons(t *testing.T) { start := date("2026-07-01") history := make([]DailyPoint, 0, 140) for index := 0; index < 140; index++ { block := index / 28 value := int64(100 + block*20) history = append(history, DailyPoint{Date: start.AddDate(0, 0, index-140), SessionCount: value, PaidSessionCount: value, PaidAmountRial: value * 1_000}) } forecast := BuildForecast(history, nil, start, 90) if forecast.Backtest.Origins < 4 || forecast.Confidence == ConfidenceInsufficientData { t.Fatalf("trend-aware rolling backtest should be usable: %+v", forecast.Backtest) } }