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

import type { Pf2eCreature } from "@initiative/domain";
import { creatureId } from "@initiative/domain";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Pf2eStatBlock } from "../pf2e-stat-block.js";

afterEach(cleanup);

const USES_PER_DAY_REGEX = /×3/;
const HEAL_DESCRIPTION_REGEX = /channel positive energy/;
const PERCEPTION_SENSES_REGEX = /\+2.*Darkvision/;
const SKILLS_REGEX = /Acrobatics \+5.*Stealth \+5/;
const SAVE_CONDITIONAL_REGEX = /\+12.*\+1 status to all saves vs\. magic/;
const SAVE_CONDITIONAL_ABSENT_REGEX = /status to all saves/;
const HP_DETAILS_REGEX = /115.*regeneration 20/;
const REGEN_REGEX = /regeneration/;
const ATTACK_NAME_REGEX = /Dogslicer/;
const ATTACK_DAMAGE_REGEX = /1d6 slashing/;
const SPELLCASTING_ENTRY_REGEX = /Divine Innate Spells\./;
const ABILITY_MID_NAME_REGEX = /Goblin Scuttle/;
const ABILITY_MID_DESC_REGEX = /The goblin Steps\./;
const CANTRIPS_REGEX = /Cantrips:/;
const AC_REGEX = /16/;
const RK_DC_13_REGEX = /DC 13/;
const RK_DC_15_REGEX = /DC 15/;
const RK_DC_25_REGEX = /DC 25/;
const RK_HUMANOID_SOCIETY_REGEX = /Humanoid \(Society\)/;
const RK_UNDEAD_RELIGION_REGEX = /Undead \(Religion\)/;
const RK_BEAST_SKILLS_REGEX = /Beast \(Arcana\/Nature\)/;
const SCROLL_NAME_REGEX = /Scroll of Teleport/;

const GOBLIN_WARRIOR: Pf2eCreature = {
	system: "pf2e",
	id: creatureId("pathfinder-monster-core:goblin-warrior"),
	name: "Goblin Warrior",
	source: "pathfinder-monster-core",
	sourceDisplayName: "Monster Core",
	level: -1,
	traits: ["small", "goblin", "humanoid"],
	perception: 2,
	senses: "Darkvision",
	languages: "Common, Goblin",
	skills: "Acrobatics +5, Athletics +2, Nature +1, Stealth +5",
	abilityMods: { str: 0, dex: 3, con: 1, int: 0, wis: -1, cha: 1 },
	ac: 16,
	saveFort: 5,
	saveRef: 7,
	saveWill: 3,
	hp: 6,
	speed: "25 feet",
	attacks: [
		{
			name: "Dogslicer",
			activity: { number: 1, unit: "action" },
			segments: [
				{
					type: "text",
					value: "+7 (agile, backstabber, finesse), 1d6 slashing",
				},
			],
		},
	],
	abilitiesMid: [
		{
			name: "Goblin Scuttle",
			activity: { number: 1, unit: "reaction" },
			segments: [{ type: "text", value: "The goblin Steps." }],
		},
	],
};

const NAUNET: Pf2eCreature = {
	system: "pf2e",
	id: creatureId("pathfinder-monster-core-2:naunet"),
	name: "Naunet",
	source: "pathfinder-monster-core-2",
	sourceDisplayName: "Monster Core 2",
	level: 7,
	traits: ["large", "monitor", "protean"],
	perception: 14,
	senses: "Darkvision",
	languages: "Chthonian, Empyrean, Protean",
	skills:
		"Acrobatics +14, Athletics +16, Intimidation +16, Stealth +14, Survival +12",
	abilityMods: { str: 5, dex: 3, con: 5, int: 0, wis: 3, cha: 3 },
	ac: 24,
	saveFort: 18,
	saveRef: 14,
	saveWill: 12,
	saveConditional: "+1 status to all saves vs. magic",
	hp: 120,
	resistances: "Precision 5, Protean anatomy 10",
	speed: "25 feet, Fly 30 feet, Swim 25 feet (unfettered movement)",
	spellcasting: [
		{
			name: "Divine Innate Spells",
			headerText: "DC 25, attack +17",
			daily: [
				{
					uses: 4,
					each: true,
					spells: [{ name: "Unfettered Movement (Constant)" }],
				},
			],
			atWill: [{ name: "Detect Magic" }],
		},
	],
};

const TROLL: Pf2eCreature = {
	system: "pf2e",
	id: creatureId("pathfinder-monster-core:forest-troll"),
	name: "Forest Troll",
	source: "pathfinder-monster-core",
	sourceDisplayName: "Monster Core",
	level: 5,
	traits: ["large", "giant", "troll"],
	perception: 11,
	senses: "Darkvision",
	languages: "Jotun",
	skills: "Athletics +12, Intimidation +12",
	abilityMods: { str: 5, dex: 2, con: 6, int: -2, wis: 0, cha: -2 },
	ac: 20,
	saveFort: 17,
	saveRef: 11,
	saveWill: 7,
	hp: 115,
	hpDetails: "regeneration 20 (deactivated by acid or fire)",
	weaknesses: "Fire 10",
	speed: "30 feet",
};

function renderStatBlock(creature: Pf2eCreature) {
	return render(<Pf2eStatBlock creature={creature} />);
}

describe("Pf2eStatBlock", () => {
	describe("header", () => {
		it("renders creature name and level", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(
				screen.getByRole("heading", { name: "Goblin Warrior" }),
			).toBeInTheDocument();
			expect(screen.getByText("Level -1")).toBeInTheDocument();
		});

		it("renders traits as tags", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("Small")).toBeInTheDocument();
			expect(screen.getByText("Goblin")).toBeInTheDocument();
			expect(screen.getByText("Humanoid")).toBeInTheDocument();
		});

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

	describe("recall knowledge", () => {
		it("renders Recall Knowledge line for a creature with a recognized type trait", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("Recall Knowledge")).toBeInTheDocument();
			expect(screen.getByText(RK_DC_13_REGEX)).toBeInTheDocument();
			expect(screen.getByText(RK_HUMANOID_SOCIETY_REGEX)).toBeInTheDocument();
		});

		it("adjusts DC for uncommon rarity", () => {
			const uncommonCreature: Pf2eCreature = {
				...GOBLIN_WARRIOR,
				traits: ["uncommon", "small", "humanoid"],
			};
			renderStatBlock(uncommonCreature);
			expect(screen.getByText(RK_DC_15_REGEX)).toBeInTheDocument();
		});

		it("adjusts DC for rare rarity", () => {
			const rareCreature: Pf2eCreature = {
				...GOBLIN_WARRIOR,
				level: 5,
				traits: ["rare", "medium", "undead"],
			};
			renderStatBlock(rareCreature);
			expect(screen.getByText(RK_DC_25_REGEX)).toBeInTheDocument();
			expect(screen.getByText(RK_UNDEAD_RELIGION_REGEX)).toBeInTheDocument();
		});

		it("shows multiple skills for types with dual skill mapping", () => {
			const beastCreature: Pf2eCreature = {
				...GOBLIN_WARRIOR,
				traits: ["small", "beast"],
			};
			renderStatBlock(beastCreature);
			expect(screen.getByText(RK_BEAST_SKILLS_REGEX)).toBeInTheDocument();
		});

		it("omits Recall Knowledge when no type trait is recognized", () => {
			const noTypeCreature: Pf2eCreature = {
				...GOBLIN_WARRIOR,
				traits: ["small", "goblin"],
			};
			renderStatBlock(noTypeCreature);
			expect(screen.queryByText("Recall Knowledge")).not.toBeInTheDocument();
		});
	});

	describe("perception and senses", () => {
		it("renders perception modifier and senses", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("Perception")).toBeInTheDocument();
			expect(screen.getByText(PERCEPTION_SENSES_REGEX)).toBeInTheDocument();
		});

		it("renders languages", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("Languages")).toBeInTheDocument();
			expect(screen.getByText("Common, Goblin")).toBeInTheDocument();
		});

		it("renders skills", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("Skills")).toBeInTheDocument();
			expect(screen.getByText(SKILLS_REGEX)).toBeInTheDocument();
		});
	});

	describe("ability modifiers", () => {
		it("renders all six ability labels", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			for (const label of ["Str", "Dex", "Con", "Int", "Wis", "Cha"]) {
				expect(screen.getByText(label)).toBeInTheDocument();
			}
		});

		it("renders positive and negative modifiers", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("+3")).toBeInTheDocument();
			expect(screen.getByText("-1")).toBeInTheDocument();
		});
	});

	describe("defenses", () => {
		it("renders AC and saves", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("AC")).toBeInTheDocument();
			expect(screen.getByText(AC_REGEX)).toBeInTheDocument();
			expect(screen.getByText("Fort")).toBeInTheDocument();
			expect(screen.getByText("Ref")).toBeInTheDocument();
			expect(screen.getByText("Will")).toBeInTheDocument();
		});

		it("renders HP", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("HP")).toBeInTheDocument();
			expect(screen.getByText("6")).toBeInTheDocument();
		});

		it("renders saveConditional inline with saves", () => {
			renderStatBlock(NAUNET);
			expect(screen.getByText(SAVE_CONDITIONAL_REGEX)).toBeInTheDocument();
		});

		it("omits saveConditional when absent", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(
				screen.queryByText(SAVE_CONDITIONAL_ABSENT_REGEX),
			).not.toBeInTheDocument();
		});

		it("renders hpDetails in parentheses after HP", () => {
			renderStatBlock(TROLL);
			expect(screen.getByText(HP_DETAILS_REGEX)).toBeInTheDocument();
		});

		it("omits hpDetails when absent", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.queryByText(REGEN_REGEX)).not.toBeInTheDocument();
		});

		it("renders resistances and weaknesses", () => {
			renderStatBlock(NAUNET);
			expect(screen.getByText("Resistances")).toBeInTheDocument();
			expect(
				screen.getByText("Precision 5, Protean anatomy 10"),
			).toBeInTheDocument();
		});
	});

	describe("abilities", () => {
		it("renders mid (defensive) abilities", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText(ABILITY_MID_NAME_REGEX)).toBeInTheDocument();
			expect(screen.getByText(ABILITY_MID_DESC_REGEX)).toBeInTheDocument();
		});
	});

	describe("speed and attacks", () => {
		it("renders speed", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText("Speed")).toBeInTheDocument();
			expect(screen.getByText("25 feet")).toBeInTheDocument();
		});

		it("renders attacks", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.getByText(ATTACK_NAME_REGEX)).toBeInTheDocument();
			expect(screen.getByText(ATTACK_DAMAGE_REGEX)).toBeInTheDocument();
		});
	});

	describe("spellcasting", () => {
		it("renders spellcasting entry with header", () => {
			renderStatBlock(NAUNET);
			expect(screen.getByText(SPELLCASTING_ENTRY_REGEX)).toBeInTheDocument();
			expect(screen.getByText("DC 25, attack +17")).toBeInTheDocument();
		});

		it("renders ranked spells", () => {
			renderStatBlock(NAUNET);
			expect(screen.getByText("Rank 4:")).toBeInTheDocument();
			expect(
				screen.getByText("Unfettered Movement (Constant)"),
			).toBeInTheDocument();
		});

		it("renders cantrips", () => {
			renderStatBlock(NAUNET);
			expect(screen.getByText("Cantrips:")).toBeInTheDocument();
			expect(screen.getByText("Detect Magic")).toBeInTheDocument();
		});

		it("omits spellcasting when absent", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(screen.queryByText(CANTRIPS_REGEX)).not.toBeInTheDocument();
		});
	});

	describe("equipment section", () => {
		const CREATURE_WITH_EQUIPMENT: Pf2eCreature = {
			...GOBLIN_WARRIOR,
			id: creatureId("test:equipped"),
			name: "Equipped NPC",
			items: "longsword, leather armor",
			equipment: [
				{
					name: "Giant Wasp Venom",
					level: 7,
					category: "poison",
					traits: ["consumable", "poison"],
					description: "A deadly poison extracted from giant wasps.",
				},
				{
					name: "Scroll of Teleport",
					level: 11,
					category: "scroll",
					traits: ["consumable", "magical", "scroll"],
					description: "A scroll containing Teleport.",
					spellName: "Teleport",
					spellRank: 6,
				},
				{
					name: "Plain Talisman",
					level: 1,
					traits: ["magical"],
				},
			],
		};

		it("renders Equipment section with item names", () => {
			renderStatBlock(CREATURE_WITH_EQUIPMENT);
			expect(
				screen.getByRole("heading", { name: "Equipment" }),
			).toBeInTheDocument();
			expect(screen.getByText("Giant Wasp Venom")).toBeInTheDocument();
		});

		it("renders scroll name as-is from Foundry data", () => {
			renderStatBlock(CREATURE_WITH_EQUIPMENT);
			expect(screen.getByText(SCROLL_NAME_REGEX)).toBeInTheDocument();
		});

		it("does not render Equipment section when creature has no equipment", () => {
			renderStatBlock(GOBLIN_WARRIOR);
			expect(
				screen.queryByRole("heading", { name: "Equipment" }),
			).not.toBeInTheDocument();
		});

		it("renders equipment items with descriptions as clickable buttons", () => {
			renderStatBlock(CREATURE_WITH_EQUIPMENT);
			expect(
				screen.getByRole("button", { name: "Giant Wasp Venom" }),
			).toBeInTheDocument();
		});

		it("renders equipment items without descriptions as plain text", () => {
			renderStatBlock(CREATURE_WITH_EQUIPMENT);
			expect(
				screen.queryByRole("button", { name: "Plain Talisman" }),
			).not.toBeInTheDocument();
			expect(screen.getByText("Plain Talisman")).toBeInTheDocument();
		});

		it("renders Items line with mundane item names", () => {
			renderStatBlock(CREATURE_WITH_EQUIPMENT);
			expect(screen.getByText("Items")).toBeInTheDocument();
			expect(screen.getByText("longsword, leather armor")).toBeInTheDocument();
		});
	});

	describe("clickable spells", () => {
		const SPELLCASTER: Pf2eCreature = {
			...NAUNET,
			id: creatureId("test:spellcaster"),
			name: "Spellcaster",
			spellcasting: [
				{
					name: "Divine Innate Spells",
					headerText: "DC 30, attack +20",
					atWill: [{ name: "Detect Magic", rank: 1 }],
					daily: [
						{
							uses: 4,
							each: true,
							spells: [
								{
									name: "Heal",
									description: "You channel positive energy to heal.",
									rank: 4,
									usesPerDay: 3,
								},
								{ name: "Restoration", rank: 4 },
							],
						},
					],
				},
			],
		};

		beforeEach(() => {
			Object.defineProperty(globalThis, "matchMedia", {
				writable: true,
				configurable: true,
				value: vi.fn().mockImplementation(() => ({
					matches: true,
					media: "(min-width: 1024px)",
					onchange: null,
					addEventListener: vi.fn(),
					removeEventListener: vi.fn(),
					addListener: vi.fn(),
					removeListener: vi.fn(),
					dispatchEvent: vi.fn(),
				})),
			});
		});

		it("renders a spell with a description as a clickable button", () => {
			renderStatBlock(SPELLCASTER);
			expect(screen.getByRole("button", { name: "Heal" })).toBeInTheDocument();
		});

		it("renders a spell without description as plain text (not a button)", () => {
			renderStatBlock(SPELLCASTER);
			expect(
				screen.queryByRole("button", { name: "Restoration" }),
			).not.toBeInTheDocument();
			expect(screen.getByText("Restoration")).toBeInTheDocument();
		});

		it("renders usesPerDay as plain text alongside the spell button", () => {
			renderStatBlock(SPELLCASTER);
			expect(screen.getByText(USES_PER_DAY_REGEX)).toBeInTheDocument();
		});

		it("opens the spell popover when a spell button is clicked", async () => {
			const user = userEvent.setup();
			renderStatBlock(SPELLCASTER);
			await user.click(screen.getByRole("button", { name: "Heal" }));
			expect(screen.getByText(HEAL_DESCRIPTION_REGEX)).toBeInTheDocument();
		});

		it("closes the popover when Escape is pressed", async () => {
			const user = userEvent.setup();
			renderStatBlock(SPELLCASTER);
			await user.click(screen.getByRole("button", { name: "Heal" }));
			expect(screen.getByText(HEAL_DESCRIPTION_REGEX)).toBeInTheDocument();
			await user.keyboard("{Escape}");
			expect(
				screen.queryByText(HEAL_DESCRIPTION_REGEX),
			).not.toBeInTheDocument();
		});
	});
});
