// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";

import type { Creature } from "@initiative/domain";
import { creatureId } from "@initiative/domain";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { StatBlock } from "../stat-block.js";

afterEach(cleanup);

const ARMOR_CLASS_REGEX = /Armor Class/;
const DEX_PLUS_4_REGEX = /Dex \+4/;
const CR_QUARTER_REGEX = /1\/4/;
const PROF_BONUS_2_REGEX = /Proficiency Bonus \+2/;
const NIMBLE_ESCAPE_REGEX = /Nimble Escape\./;
const SCIMITAR_REGEX = /Scimitar\./;
const DETECT_REGEX = /Detect\./;
const TAIL_ATTACK_REGEX = /Tail Attack\./;
const INNATE_SPELLCASTING_REGEX = /Innate Spellcasting\./;
const AT_WILL_REGEX = /At Will:/;
const DETECT_MAGIC_REGEX = /detect magic, suggestion/;
const DAILY_REGEX = /3\/day each:/;
const FIREBALL_REGEX = /fireball, wall of fire/;
const LONG_REST_REGEX = /1\/long rest:/;
const WISH_REGEX = /wish/;

const GOBLIN: Creature = {
	id: creatureId("srd:goblin"),
	name: "Goblin",
	source: "MM",
	sourceDisplayName: "Monster Manual",
	size: "Small",
	type: "humanoid",
	alignment: "neutral evil",
	ac: 15,
	acSource: "leather armor, shield",
	hp: { average: 7, formula: "2d6" },
	speed: "30 ft.",
	abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 },
	cr: "1/4",
	initiativeProficiency: 0,
	proficiencyBonus: 2,
	passive: 9,
	savingThrows: "Dex +4",
	skills: "Stealth +6",
	senses: "darkvision 60 ft., passive Perception 9",
	languages: "Common, Goblin",
	traits: [{ name: "Nimble Escape", text: "Disengage or Hide as bonus." }],
	actions: [{ name: "Scimitar", text: "Melee: +4 to hit, 5 slashing." }],
	bonusActions: [{ name: "Nimble", text: "Disengage or Hide." }],
	reactions: [{ name: "Redirect", text: "Redirect attack to ally." }],
};

const DRAGON: Creature = {
	id: creatureId("srd:dragon"),
	name: "Ancient Red Dragon",
	source: "MM",
	sourceDisplayName: "Monster Manual",
	size: "Gargantuan",
	type: "dragon",
	alignment: "chaotic evil",
	ac: 22,
	hp: { average: 546, formula: "28d20 + 252" },
	speed: "40 ft., climb 40 ft., fly 80 ft.",
	abilities: { str: 30, dex: 10, con: 29, int: 18, wis: 15, cha: 23 },
	cr: "24",
	initiativeProficiency: 0,
	proficiencyBonus: 7,
	passive: 26,
	resist: "fire",
	immune: "fire",
	vulnerable: "cold",
	conditionImmune: "frightened",
	legendaryActions: {
		preamble: "The dragon can take 3 legendary actions.",
		entries: [
			{ name: "Detect", text: "Wisdom (Perception) check." },
			{ name: "Tail Attack", text: "Tail attack." },
		],
	},
	spellcasting: [
		{
			name: "Innate Spellcasting",
			headerText: "The dragon's spellcasting ability is Charisma.",
			atWill: ["detect magic", "suggestion"],
			daily: [{ uses: 3, each: true, spells: ["fireball", "wall of fire"] }],
			restLong: [{ uses: 1, each: false, spells: ["wish"] }],
		},
	],
};

function renderStatBlock(creature: Creature) {
	return render(<StatBlock creature={creature} />);
}

describe("StatBlock", () => {
	describe("header", () => {
		it("renders creature name", () => {
			renderStatBlock(GOBLIN);
			expect(
				screen.getByRole("heading", { name: "Goblin" }),
			).toBeInTheDocument();
		});

		it("renders size, type, alignment", () => {
			renderStatBlock(GOBLIN);
			expect(
				screen.getByText("Small humanoid, neutral evil"),
			).toBeInTheDocument();
		});

		it("renders source display name", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("Monster Manual")).toBeInTheDocument();
		});
	});

	describe("stats bar", () => {
		it("renders AC with source", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText(ARMOR_CLASS_REGEX)).toBeInTheDocument();
			expect(screen.getByText("15")).toBeInTheDocument();
			expect(screen.getByText("(leather armor, shield)")).toBeInTheDocument();
		});

		it("renders AC without source when acSource is undefined", () => {
			renderStatBlock(DRAGON);
			expect(screen.getByText("22")).toBeInTheDocument();
		});

		it("renders HP average and formula", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("7")).toBeInTheDocument();
			expect(screen.getByText("(2d6)")).toBeInTheDocument();
		});

		it("renders speed", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("30 ft.")).toBeInTheDocument();
		});
	});

	describe("ability scores", () => {
		it("renders all 6 ability labels", () => {
			renderStatBlock(GOBLIN);
			for (const label of ["STR", "DEX", "CON", "INT", "WIS", "CHA"]) {
				expect(screen.getByText(label)).toBeInTheDocument();
			}
		});

		it("renders ability scores with modifier notation", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("(+2)")).toBeInTheDocument();
		});
	});

	describe("properties", () => {
		it("renders saving throws when present", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("Saving Throws")).toBeInTheDocument();
			expect(screen.getByText(DEX_PLUS_4_REGEX)).toBeInTheDocument();
		});

		it("renders skills when present", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("Skills")).toBeInTheDocument();
		});

		it("renders damage resistances, immunities, vulnerabilities", () => {
			renderStatBlock(DRAGON);
			expect(screen.getByText("Damage Resistances")).toBeInTheDocument();
			expect(screen.getByText("Damage Immunities")).toBeInTheDocument();
			expect(screen.getByText("Damage Vulnerabilities")).toBeInTheDocument();
			expect(screen.getByText("Condition Immunities")).toBeInTheDocument();
		});

		it("omits properties when undefined", () => {
			renderStatBlock(GOBLIN);
			expect(screen.queryByText("Damage Resistances")).not.toBeInTheDocument();
			expect(screen.queryByText("Damage Immunities")).not.toBeInTheDocument();
		});

		it("renders CR and proficiency bonus", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText("Challenge")).toBeInTheDocument();
			expect(screen.getByText(CR_QUARTER_REGEX)).toBeInTheDocument();
			expect(screen.getByText(PROF_BONUS_2_REGEX)).toBeInTheDocument();
		});
	});

	describe("traits", () => {
		it("renders trait entries", () => {
			renderStatBlock(GOBLIN);
			expect(screen.getByText(NIMBLE_ESCAPE_REGEX)).toBeInTheDocument();
		});
	});

	describe("actions / bonus actions / reactions", () => {
		it("renders actions heading and entries", () => {
			renderStatBlock(GOBLIN);
			expect(
				screen.getByRole("heading", { name: "Actions" }),
			).toBeInTheDocument();
			expect(screen.getByText(SCIMITAR_REGEX)).toBeInTheDocument();
		});

		it("renders bonus actions heading and entries", () => {
			renderStatBlock(GOBLIN);
			expect(
				screen.getByRole("heading", { name: "Bonus Actions" }),
			).toBeInTheDocument();
		});

		it("renders reactions heading and entries", () => {
			renderStatBlock(GOBLIN);
			expect(
				screen.getByRole("heading", { name: "Reactions" }),
			).toBeInTheDocument();
		});
	});

	describe("legendary actions", () => {
		it("renders legendary actions with preamble", () => {
			renderStatBlock(DRAGON);
			expect(
				screen.getByRole("heading", { name: "Legendary Actions" }),
			).toBeInTheDocument();
			expect(
				screen.getByText("The dragon can take 3 legendary actions."),
			).toBeInTheDocument();
			expect(screen.getByText(DETECT_REGEX)).toBeInTheDocument();
			expect(screen.getByText(TAIL_ATTACK_REGEX)).toBeInTheDocument();
		});

		it("omits legendary actions when undefined", () => {
			renderStatBlock(GOBLIN);
			expect(
				screen.queryByRole("heading", { name: "Legendary Actions" }),
			).not.toBeInTheDocument();
		});
	});

	describe("spellcasting", () => {
		it("renders spellcasting block with header", () => {
			renderStatBlock(DRAGON);
			expect(screen.getByText(INNATE_SPELLCASTING_REGEX)).toBeInTheDocument();
		});

		it("renders at-will spells", () => {
			renderStatBlock(DRAGON);
			expect(screen.getByText(AT_WILL_REGEX)).toBeInTheDocument();
			expect(screen.getByText(DETECT_MAGIC_REGEX)).toBeInTheDocument();
		});

		it("renders daily spells", () => {
			renderStatBlock(DRAGON);
			expect(screen.getByText(DAILY_REGEX)).toBeInTheDocument();
			expect(screen.getByText(FIREBALL_REGEX)).toBeInTheDocument();
		});

		it("renders long rest spells", () => {
			renderStatBlock(DRAGON);
			expect(screen.getByText(LONG_REST_REGEX)).toBeInTheDocument();
			expect(screen.getByText(WISH_REGEX)).toBeInTheDocument();
		});

		it("omits spellcasting when undefined", () => {
			renderStatBlock(GOBLIN);
			expect(screen.queryByText(AT_WILL_REGEX)).not.toBeInTheDocument();
		});
	});
});
