import { describe, expect, test } from "bun:test"; import { buildVoiceLeaderboard, buildXpLeaderboard, formatInteger, formatVoiceDuration, normalizeRequestedPage, shortenDiscordId, type VoiceSourceRow, type XpSourceRow, } from "@/lib/leaderboard/ranking"; function xpRow( userId: string, totalXp: string, level = 1, displayName = userId, ): XpSourceRow { return { userId, username: `${userId}-username`, displayName, level, totalXp, }; } function voiceRow( userId: string, channelId: string, totalSeconds: string, displayName: string | null = userId, ): VoiceSourceRow { return { userId, channelId, totalSeconds, displayName, username: displayName ? `${userId}-username` : null, }; } describe("XP leaderboard", () => { test("filters level zero and assigns shared competition ranks", () => { const result = buildXpLeaderboard([ xpRow("b", "900", 2, "Beta"), xpRow("a", "1000", 3, "Alpha"), xpRow("c", "900", 2, "Charlie"), xpRow("hidden", "9999", 0), xpRow("d", "800", 1, "Delta"), ], 1); expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([ ["a", 1], ["b", 2], ["c", 2], ["d", 4], ]); }); test("returns ten rows and normalizes pages beyond the result", () => { const rows = Array.from({ length: 101 }, (_, index) => xpRow(String(index).padStart(3, "0"), String(1_000 - index))); const middle = buildXpLeaderboard(rows, 2); expect(middle.rows).toHaveLength(10); expect(middle.rows[0]?.rank).toBe(11); expect(middle.totalPages).toBe(11); const last = buildXpLeaderboard(rows, 99); expect(last.page).toBe(11); expect(last.rows).toHaveLength(1); }); test("uses stable name then user ID ordering for ties", () => { const result = buildXpLeaderboard([ xpRow("3", "100", 1, "Zulu"), xpRow("2", "100", 1, "Alpha"), xpRow("1", "100", 1, "Alpha"), ], 1); expect(result.rows.map((row) => row.userId)).toEqual(["1", "2", "3"]); }); test("searches identities while preserving overall ranks", () => { const result = buildXpLeaderboard([ xpRow("top", "1000", 4, "Alpha"), xpRow("target", "900", 3, "Beta"), xpRow("third", "800", 2, "Gamma"), ], 1, "target-user"); expect(result.totalRows).toBe(1); expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([ ["target", 2], ]); }); }); describe("voice leaderboard", () => { const rows = [ voiceRow("a", "voice-1", "100"), voiceRow("a", "voice-2", "200"), voiceRow("a", "deleted-channel", "9_999"), voiceRow("b", "voice-1", "300"), voiceRow("missing", "voice-2", "60", null), ]; test("aggregates current channels and excludes historical channels", () => { const result = buildVoiceLeaderboard(rows, ["voice-1", "voice-2"], 1); expect(result.rows.map(({ userId, totalSeconds, rank }) => ({ userId, totalSeconds, rank, }))).toEqual([ { userId: "a", totalSeconds: "300", rank: 1 }, { userId: "b", totalSeconds: "300", rank: 1 }, { userId: "missing", totalSeconds: "60", rank: 3 }, ]); }); test("filters to a selected current channel", () => { const result = buildVoiceLeaderboard( rows, ["voice-1", "voice-2"], 1, "voice-1", ); expect(result.rows.map(({ userId, totalSeconds }) => [ userId, totalSeconds, ])).toEqual([ ["b", "300"], ["a", "100"], ]); }); test("keeps unresolved identities and formats their shortened ID", () => { const result = buildVoiceLeaderboard(rows, ["voice-2"], 1); const unresolved = result.rows.find((row) => row.userId === "missing"); expect(unresolved?.displayName).toBeNull(); expect(unresolved?.username).toBeNull(); expect(shortenDiscordId("123456789012345678")).toBe("12345…5678"); }); test("searches aggregated voice rows", () => { const result = buildVoiceLeaderboard( rows, ["voice-1", "voice-2"], 1, undefined, "missing", ); expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([ ["missing", 3], ]); }); }); describe("leaderboard formatting", () => { test("normalizes invalid page values", () => { expect(normalizeRequestedPage(undefined)).toBe(1); expect(normalizeRequestedPage("0")).toBe(1); expect(normalizeRequestedPage("2.5")).toBe(1); expect(normalizeRequestedPage(["2", "3"])).toBe(1); expect(normalizeRequestedPage("3")).toBe(3); }); test("formats large integer and voice duration values", () => { expect(formatInteger("1234567")).toBe("1,234,567"); expect(formatVoiceDuration("90061")).toBe("1 วัน 1 ชม. 1 นาที"); expect(formatVoiceDuration("30")).toBe("น้อยกว่า 1 นาที"); }); });