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");
	});
});
