package domain import ( "fmt" "strings" "testing" ) func TestXpForLevel(t *testing.T) { cases := []struct { n int want int64 }{ {0, 0}, {1, 100}, {2, 300}, {5, 1500}, {10, 5500}, {27, 37800}, {50, 127500}, } for _, c := range cases { got := XpForLevel(c.n) if got != c.want { t.Errorf("XpForLevel(%d) = %d, want %d", c.n, got, c.want) } } } func TestLevelForXP(t *testing.T) { cases := []struct { xp int64 want int }{ {0, 0}, {50, 0}, {99, 0}, {100, 1}, {299, 1}, {300, 2}, {5500, 10}, {37800, 27}, {127500, 50}, } for _, c := range cases { got := LevelForXP(c.xp) if got != c.want { t.Errorf("LevelForXP(%d) = %d, want %d", c.xp, got, c.want) } } } func TestLevelForXP_Monotonic(t *testing.T) { for n := 0; n <= 100; n++ { xp := XpForLevel(n) l := LevelForXP(xp) if l != n { t.Errorf("LevelForXP(XpForLevel(%d)) = %d, want %d", n, l, n) } } } func TestXpForWorkSeconds(t *testing.T) { if got := XpForWorkSeconds(0); got != 0 { t.Errorf("XpForWorkSeconds(0) = %d, want 0", got) } if got := XpForWorkSeconds(3600); got != 3600 { t.Errorf("XpForWorkSeconds(3600) = %d, want 3600", got) } if got := XpForWorkSeconds(1800); got != 1800 { t.Errorf("XpForWorkSeconds(1800) = %d, want 1800", got) } } func TestStreakBonus(t *testing.T) { cases := []struct { streak int want int64 }{ {0, 0}, {1, 50}, {10, 500}, {5, 250}, {20, 500}, } for _, c := range cases { got := StreakBonus(c.streak) if got != c.want { t.Errorf("StreakBonus(%d) = %d, want %d", c.streak, got, c.want) } } } func TestCurrencySymbol(t *testing.T) { cases := []struct { currency string want string }{ {"", "$"}, {"$", "$"}, {"$ USD", "$"}, {"\u20AC EUR", "\u20AC"}, {"\u00A3", "\u00A3"}, {"Toman", "Toman"}, {" ", "$"}, } for _, c := range cases { got := CurrencySymbol(c.currency) if got != c.want { t.Errorf("CurrencySymbol(%q) = %q, want %q", c.currency, got, c.want) } } } func TestComputeDailySalary(t *testing.T) { got := ComputeDailySalary(3600, 25.0) if got != 25.0 { t.Errorf("ComputeDailySalary(3600, 25) = %.2f, want 25.00", got) } got = ComputeDailySalary(9000, 20.0) if got != 50.0 { t.Errorf("ComputeDailySalary(9000, 20) = %.2f, want 50.00", got) } got = ComputeDailySalary(0, 50.0) if got != 0 { t.Errorf("ComputeDailySalary(0, 50) = %.2f, want 0", got) } got = ComputeDailySalary(3599, 10.0) if got != 10.0 { t.Errorf("ComputeDailySalary(3599, 10) = %.2f, want 10.00", got) } } func TestSanitizeNote(t *testing.T) { if got := SanitizeNote(" hello "); got != "hello" { t.Errorf("SanitizeNote trim: got %q", got) } if got := SanitizeNote("he\x00llo\nworld\t!"); got != "hello\nworld\t!" { t.Errorf("SanitizeNote control: got %q", got) } if got := SanitizeNote("line1\nline2\tindented"); got != "line1\nline2\tindented" { t.Errorf("SanitizeNote newline/tab: got %q", got) } long := make([]byte, 600) for i := range long { long[i] = 'a' } got := SanitizeNote(string(long)) if len(got) > 500 { t.Errorf("SanitizeNote length: got %d, want <= 500", len(got)) } } func TestSanitizeDisplayName(t *testing.T) { if got := SanitizeDisplayName(" Alice "); got != "Alice" { t.Errorf("SanitizeDisplayName trim: got %q", got) } if got := SanitizeDisplayName("Ali\x00ce"); got != "Alice" { t.Errorf("SanitizeDisplayName control: got %q", got) } if got := SanitizeDisplayName(""); got != "" { t.Errorf("SanitizeDisplayName empty: got %q", got) } } func TestParseCurrencyInput(t *testing.T) { cases := []struct { input string want string ok bool }{ {"$ USD", "$ USD", true}, {"T Toman", "T Toman", true}, {"IRR Rial", "IRR Rial", true}, {"\u20AC EUR", "\u20AC EUR", true}, {"", "", false}, {"$", "", false}, {" ", "", false}, {"$$$ TooLongCode", "", false}, } for _, c := range cases { got, errMsg := ParseCurrencyInput(c.input) if c.ok && (got != c.want || errMsg != "") { t.Errorf("ParseCurrencyInput(%q) = (%q, %q), want (%q, \"\")", c.input, got, errMsg, c.want) } if !c.ok && errMsg == "" { t.Errorf("ParseCurrencyInput(%q) = (%q, \"\"), want error", c.input, got) } } } func TestComputeTrendFromAvgs(t *testing.T) { cases := []struct { recent, older float64 wantPts int wantDirSub string }{ {0, 0, 5, "stable"}, {100, 0, 15, "new"}, {0, 100, 0, "-100"}, {121, 100, 15, "+21"}, {100, 100, 5, "stable"}, {100, 130, 0, "-23"}, } for _, c := range cases { pts, dir := ComputeTrendFromAvgs(c.recent, c.older) if pts != c.wantPts { t.Errorf("ComputeTrendFromAvgs(%.0f, %.0f) pts = %d, want %d", c.recent, c.older, pts, c.wantPts) } if !strings.Contains(dir, c.wantDirSub) { t.Errorf("ComputeTrendFromAvgs(%.0f, %.0f) dir = %q, want containing %q", c.recent, c.older, dir, c.wantDirSub) } } } func TestComputeStreakFromDates_Empty(t *testing.T) { cur, longest := ComputeStreakFromDates(nil, "2026-06-25") if cur != 0 || longest != 0 { t.Errorf("empty dates: got (%d, %d), want (0, 0)", cur, longest) } } func TestComputeStreakFromDates_CurrentOnly(t *testing.T) { dates := []string{"2026-06-25", "2026-06-26"} cur, longest := ComputeStreakFromDates(dates, "2026-06-26") if cur != 2 { t.Errorf("current streak = %d, want 2", cur) } if longest != 2 { t.Errorf("longest streak = %d, want 2", longest) } } func TestComputeStreakFromDates_GapBreaksStreak(t *testing.T) { dates := []string{"2026-06-23", "2026-06-25", "2026-06-26"} cur, longest := ComputeStreakFromDates(dates, "2026-06-26") if cur != 2 { t.Errorf("current streak = %d, want 2 (today and yesterday)", cur) } if longest != 2 { t.Errorf("longest streak = %d, want 2", longest) } } func TestComputeStreakFromDates_NoEventsToday(t *testing.T) { dates := []string{"2026-06-24", "2026-06-25"} cur, longest := ComputeStreakFromDates(dates, "2026-06-26") if cur != 0 { t.Errorf("current streak = %d, want 0 (no events today)", cur) } if longest != 2 { t.Errorf("longest streak = %d, want 2", longest) } } func TestComputeStreakFromDates_LongStreak(t *testing.T) { dates := make([]string, 0, 10) for d := 1; d <= 10; d++ { dates = append(dates, fmt.Sprintf("2026-06-%02d", d)) } cur, longest := ComputeStreakFromDates(dates, "2026-06-10") if cur != 10 { t.Errorf("current streak = %d, want 10", cur) } if longest != 10 { t.Errorf("longest streak = %d, want 10", longest) } } func TestComputeStreakFromDates_LongestNotCurrent(t *testing.T) { dates := []string{} for d := 1; d <= 5; d++ { dates = append(dates, formatDateInt(2026, 6, d)) } dates = append(dates, "2026-06-09", "2026-06-10") cur, longest := ComputeStreakFromDates(dates, "2026-06-10") if cur != 2 { t.Errorf("current streak = %d, want 2", cur) } if longest != 5 { t.Errorf("longest streak = %d, want 5", longest) } } func formatDateInt(y, m, d int) string { return FormatDateForCalendar( func() string { s := make([]byte, 10) s[0] = byte('0' + y/1000) s[1] = byte('0' + (y/100)%10) s[2] = byte('0' + (y/10)%10) s[3] = byte('0' + y%10) s[4] = '-' s[5] = byte('0' + m/10) s[6] = byte('0' + m%10) s[7] = '-' s[8] = byte('0' + d/10) s[9] = byte('0' + d%10) return string(s) }(), "gregorian") }