import { describe, test, expect } from "bun:test"; import { evaluateGuess, getBookById, getFirstLetter, getGrade, getNextGradeMessage, isAdjacent, toOrdinal, } from "$lib/utils/game"; describe("getBookById", () => { test("returns correct book for a valid ID", () => { const book = getBookById("GEN"); expect(book).toBeDefined(); expect(book!.name).toBe("Genesis"); expect(book!.order).toBe(1); }); test("returns the last book by ID", () => { const book = getBookById("REV"); expect(book).toBeDefined(); expect(book!.name).toBe("Revelation"); expect(book!.order).toBe(66); }); test("returns undefined for an invalid ID", () => { expect(getBookById("INVALID")).toBeUndefined(); }); test("returns undefined for an empty string", () => { expect(getBookById("")).toBeUndefined(); }); test("is case-sensitive", () => { expect(getBookById("gen")).toBeUndefined(); }); }); describe("isAdjacent", () => { test("consecutive books are adjacent", () => { expect(isAdjacent("GEN", "EXO")).toBe(true); // 1, 2 }); test("adjacency is symmetric", () => { expect(isAdjacent("EXO", "GEN")).toBe(true); }); test("books two apart are not adjacent", () => { expect(isAdjacent("GEN", "LEV")).toBe(false); // 1, 3 }); test("the same book is not adjacent to itself", () => { expect(isAdjacent("GEN", "GEN")).toBe(false); // diff = 0 }); test("works across testament boundary (Malachi / Matthew)", () => { expect(isAdjacent("MAL", "MAT")).toBe(true); // 39, 40 }); test("far-apart books are not adjacent", () => { expect(isAdjacent("GEN", "REV")).toBe(false); }); test("returns false for unknown IDs", () => { expect(isAdjacent("FAKE", "GEN")).toBe(false); expect(isAdjacent("GEN", "FAKE")).toBe(false); }); }); describe("getFirstLetter", () => { test("returns first letter of a normal book name", () => { expect(getFirstLetter("Genesis")).toBe("G"); expect(getFirstLetter("Revelation")).toBe("R"); }); test("skips leading digits and returns first letter", () => { expect(getFirstLetter("1 Samuel")).toBe("S"); expect(getFirstLetter("2 Kings")).toBe("K"); expect(getFirstLetter("1 Corinthians")).toBe("C"); expect(getFirstLetter("3 John")).toBe("J"); }); test("returns first letter of multi-word names", () => { expect(getFirstLetter("Song of Solomon")).toBe("S"); }); }); describe("evaluateGuess", () => { test("returns null for an invalid guess ID", () => { expect(evaluateGuess("INVALID", "GEN")).toBeNull(); }); test("returns null for an invalid correct ID", () => { expect(evaluateGuess("GEN", "INVALID")).toBeNull(); }); test("exact book match: testamentMatch and sectionMatch are true, adjacent is false", () => { const result = evaluateGuess("GEN", "GEN"); expect(result).not.toBeNull(); expect(result!.book.id).toBe("GEN"); expect(result!.testamentMatch).toBe(true); expect(result!.sectionMatch).toBe(true); expect(result!.adjacent).toBe(false); }); test("same section implies same testament", () => { // Genesis and Exodus are both OT Law const result = evaluateGuess("GEN", "EXO"); expect(result!.testamentMatch).toBe(true); expect(result!.sectionMatch).toBe(true); expect(result!.adjacent).toBe(true); }); test("same testament, different section", () => { // Genesis (Law) vs Joshua (History) — both OT const result = evaluateGuess("GEN", "JOS"); expect(result!.testamentMatch).toBe(true); expect(result!.sectionMatch).toBe(false); expect(result!.adjacent).toBe(false); }); test("different testament, no match", () => { // Genesis (OT) vs Matthew (NT) const result = evaluateGuess("GEN", "MAT"); expect(result!.testamentMatch).toBe(false); expect(result!.sectionMatch).toBe(false); expect(result!.adjacent).toBe(false); }); test("adjacent books across testament boundary", () => { // Malachi (OT, 39) and Matthew (NT, 40) const result = evaluateGuess("MAL", "MAT"); expect(result!.adjacent).toBe(true); expect(result!.testamentMatch).toBe(false); expect(result!.sectionMatch).toBe(false); }); test("adjacent books within same testament and section", () => { // Hosea (28) and Joel (29), both Minor Prophets const result = evaluateGuess("HOS", "JOL"); expect(result!.adjacent).toBe(true); expect(result!.testamentMatch).toBe(true); expect(result!.sectionMatch).toBe(true); }); test("firstLetterMatch: same first letter", () => { // Genesis and Galatians both start with G const result = evaluateGuess("GEN", "GAL"); expect(result!.firstLetterMatch).toBe(true); }); test("firstLetterMatch: different first letter", () => { // Genesis (G) vs Matthew (M) const result = evaluateGuess("GEN", "MAT"); expect(result!.firstLetterMatch).toBe(false); }); test("firstLetterMatch is case-insensitive", () => { // Both start with J but from different contexts — Jeremiah vs Joel const result = evaluateGuess("JER", "JOL"); expect(result!.firstLetterMatch).toBe(true); }); test("special case: two Epistle '1' books always firstLetterMatch", () => { // 1 Corinthians (Pauline) vs 1 John (General) — both Epistles starting with "1" const result = evaluateGuess("1CO", "1JN"); expect(result!.firstLetterMatch).toBe(true); }); test("special case: Epistle '1' book vs non-Epistle '1' book — no special treatment", () => { // 1 Corinthians (Pauline Epistles) vs 1 Samuel (History) // Correct is NOT Epistles, so special case doesn't apply const result = evaluateGuess("1CO", "1SA"); // getFirstLetter("1 Corinthians") = "C", getFirstLetter("1 Samuel") = "S" → false expect(result!.firstLetterMatch).toBe(false); }); test("special case only triggers when BOTH are Epistle '1' books", () => { // 2 Corinthians (Pauline, starts with "2") vs 1 John (General, starts with "1") // guessIsEpistlesWithNumber requires name[0] === "1", so 2CO fails const result = evaluateGuess("2CO", "1JN"); // getFirstLetter("2 Corinthians") = "C", getFirstLetter("1 John") = "J" → false expect(result!.firstLetterMatch).toBe(false); }); }); describe("getGrade", () => { test("1 guess → S+", () => expect(getGrade(1)).toBe("S+")); test("2 guesses → A+", () => expect(getGrade(2)).toBe("A+")); test("3 guesses → A", () => expect(getGrade(3)).toBe("A")); test("4 guesses → B+", () => expect(getGrade(4)).toBe("B+")); test("6 guesses → B+", () => expect(getGrade(6)).toBe("B+")); test("7 guesses → B", () => expect(getGrade(7)).toBe("B")); test("10 guesses → B", () => expect(getGrade(10)).toBe("B")); test("11 guesses → C+", () => expect(getGrade(11)).toBe("C+")); test("15 guesses → C+", () => expect(getGrade(15)).toBe("C+")); test("16 guesses → C", () => expect(getGrade(16)).toBe("C")); test("100 guesses → C", () => expect(getGrade(100)).toBe("C")); }); describe("getNextGradeMessage", () => { test("returns empty string at top grade", () => { expect(getNextGradeMessage(1)).toBe(""); }); test("grade A+ shows 1 guess threshold", () => { expect(getNextGradeMessage(2)).toBe("Next grade: 1 guess or less"); }); test("grade A shows 2 guess threshold", () => { expect(getNextGradeMessage(3)).toBe("Next grade: 2 guesses or less"); }); test("grade B+ shows 3 guess threshold", () => { expect(getNextGradeMessage(4)).toBe("Next grade: 3 guesses or less"); expect(getNextGradeMessage(6)).toBe("Next grade: 3 guesses or less"); }); test("grade B shows 6 guess threshold", () => { expect(getNextGradeMessage(7)).toBe("Next grade: 6 guesses or less"); expect(getNextGradeMessage(10)).toBe("Next grade: 6 guesses or less"); }); test("grade C+ shows 10 guess threshold", () => { expect(getNextGradeMessage(11)).toBe("Next grade: 10 guesses or less"); expect(getNextGradeMessage(15)).toBe("Next grade: 10 guesses or less"); }); test("grade C shows 15 guess threshold", () => { expect(getNextGradeMessage(16)).toBe("Next grade: 15 guesses or less"); expect(getNextGradeMessage(50)).toBe("Next grade: 15 guesses or less"); }); }); describe("toOrdinal", () => { test("1st, 2nd, 3rd", () => { expect(toOrdinal(1)).toBe("1st"); expect(toOrdinal(2)).toBe("2nd"); expect(toOrdinal(3)).toBe("3rd"); }); test("4-10 use th", () => { expect(toOrdinal(4)).toBe("4th"); expect(toOrdinal(10)).toBe("10th"); }); test("11, 12, 13 use th (not st/nd/rd)", () => { expect(toOrdinal(11)).toBe("11th"); expect(toOrdinal(12)).toBe("12th"); expect(toOrdinal(13)).toBe("13th"); }); test("21, 22, 23 use st/nd/rd", () => { expect(toOrdinal(21)).toBe("21st"); expect(toOrdinal(22)).toBe("22nd"); expect(toOrdinal(23)).toBe("23rd"); }); test("101, 102, 103 use st/nd/rd", () => { expect(toOrdinal(101)).toBe("101st"); expect(toOrdinal(102)).toBe("102nd"); expect(toOrdinal(103)).toBe("103rd"); }); test("111, 112, 113 use th", () => { expect(toOrdinal(111)).toBe("111th"); expect(toOrdinal(112)).toBe("112th"); expect(toOrdinal(113)).toBe("113th"); }); });