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

import { type CreatureId, combatantId } from "@initiative/domain";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { AllProviders } from "../../__tests__/test-providers.js";
import { CombatantRow } from "../combatant-row.js";
import { PLAYER_COLOR_HEX } from "../player-icon-map.js";

const TEMP_HP_REGEX = /^\+\d/;
const CURRENT_HP_7_REGEX = /Current HP: 7/;
const CURRENT_HP_REGEX = /Current HP/;

// DOM API stubs
beforeAll(() => {
	Object.defineProperty(globalThis, "matchMedia", {
		writable: true,
		value: vi.fn().mockImplementation((query: string) => ({
			matches: false,
			media: query,
			onchange: null,
			addListener: vi.fn(),
			removeListener: vi.fn(),
			addEventListener: vi.fn(),
			removeEventListener: vi.fn(),
			dispatchEvent: vi.fn(),
		})),
	});
});

afterEach(cleanup);

function renderRow(
	overrides: Partial<{
		combatant: Parameters<typeof CombatantRow>[0]["combatant"];
		isActive: boolean;
	}> = {},
) {
	const combatant = overrides.combatant ?? {
		id: combatantId("1"),
		name: "Goblin",
		initiative: 15,
		maxHp: 10,
		currentHp: 10,
		ac: 13,
	};
	return render(
		<CombatantRow
			combatant={combatant}
			isActive={overrides.isActive ?? false}
		/>,
		{ wrapper: AllProviders },
	);
}

describe("CombatantRow", () => {
	it("renders combatant name", () => {
		renderRow();
		expect(screen.getByText("Goblin")).toBeInTheDocument();
	});

	it("renders initiative value", () => {
		renderRow();
		expect(screen.getByText("15")).toBeInTheDocument();
	});

	it("renders current HP", () => {
		renderRow({
			combatant: {
				id: combatantId("1"),
				name: "Goblin",
				maxHp: 10,
				currentHp: 7,
			},
		});
		expect(screen.getByText("7")).toBeInTheDocument();
	});

	it("active combatant gets active border styling", () => {
		const { container } = renderRow({ isActive: true });
		const row = container.firstElementChild;
		expect(row?.className).toContain("border-active-row-border");
	});

	it("unconscious combatant (currentHp === 0) gets dimmed styling", () => {
		renderRow({
			combatant: {
				id: combatantId("1"),
				name: "Goblin",
				maxHp: 10,
				currentHp: 0,
			},
		});
		// The name area should have opacity-50
		const nameEl = screen.getByText("Goblin");
		const nameContainer = nameEl.closest(".opacity-50");
		expect(nameContainer).not.toBeNull();
	});

	it("shows 'Max' placeholder when no maxHp is set", () => {
		renderRow({
			combatant: {
				id: combatantId("1"),
				name: "Goblin",
			},
		});
		expect(screen.getByText("Max")).toBeInTheDocument();
	});

	it("shows concentration icon when isConcentrating is true", () => {
		renderRow({
			combatant: {
				id: combatantId("1"),
				name: "Goblin",
				isConcentrating: true,
			},
		});
		const concButton = screen.getByRole("button", {
			name: "Toggle concentration",
		});
		expect(concButton.className).toContain("text-purple-400");
	});

	it("shows player character icon and color when set", () => {
		const { container } = renderRow({
			combatant: {
				id: combatantId("1"),
				name: "Aragorn",
				color: "red",
				icon: "sword",
			},
		});
		// The icon should be rendered with the player color
		const svgIcon = container.querySelector("svg[style]");
		expect(svgIcon).not.toBeNull();
		expect(svgIcon).toHaveStyle({ color: PLAYER_COLOR_HEX.red });
	});

	it("remove button removes after confirmation", async () => {
		const user = userEvent.setup();
		renderRow();
		const removeBtn = screen.getByRole("button", {
			name: "Remove combatant",
		});
		// First click enters confirm state
		await user.click(removeBtn);
		// Second click confirms
		const confirmBtn = screen.getByRole("button", {
			name: "Confirm remove combatant",
		});
		await user.click(confirmBtn);
		// After confirming, the button returns to its initial state
		expect(
			screen.queryByRole("button", { name: "Confirm remove combatant" }),
		).not.toBeInTheDocument();
	});

	it("shows d20 roll button when initiative is undefined and combatant has creatureId", () => {
		renderRow({
			combatant: {
				id: combatantId("1"),
				name: "Goblin",
				creatureId: "srd:goblin" as CreatureId,
			},
		});
		expect(
			screen.getByRole("button", { name: "Roll initiative" }),
		).toBeInTheDocument();
	});

	describe("concentration pulse", () => {
		it("pulses when currentHp drops on a concentrating combatant", () => {
			const combatant = {
				id: combatantId("1"),
				name: "Goblin",
				maxHp: 20,
				currentHp: 15,
				isConcentrating: true,
			};
			const { rerender, container } = renderRow({ combatant });
			rerender(
				<CombatantRow
					combatant={{ ...combatant, currentHp: 10 }}
					isActive={false}
				/>,
			);
			const row = container.firstElementChild;
			expect(row?.className).toContain("animate-concentration-pulse");
		});

		it("does not pulse when not concentrating", () => {
			const combatant = {
				id: combatantId("1"),
				name: "Goblin",
				maxHp: 20,
				currentHp: 15,
				isConcentrating: false,
			};
			const { rerender, container } = renderRow({ combatant });
			rerender(
				<CombatantRow
					combatant={{ ...combatant, currentHp: 10 }}
					isActive={false}
				/>,
			);
			const row = container.firstElementChild;
			expect(row?.className).not.toContain("animate-concentration-pulse");
		});

		it("pulses when temp HP absorbs all damage on a concentrating combatant", () => {
			const combatant = {
				id: combatantId("1"),
				name: "Goblin",
				maxHp: 20,
				currentHp: 15,
				tempHp: 8,
				isConcentrating: true,
			};
			const { rerender, container } = renderRow({ combatant });
			// Temp HP absorbs all damage, currentHp unchanged
			rerender(
				<CombatantRow
					combatant={{ ...combatant, tempHp: 3 }}
					isActive={false}
				/>,
			);
			const row = container.firstElementChild;
			expect(row?.className).toContain("animate-concentration-pulse");
		});
	});

	describe("inline name editing", () => {
		it("click rename → type new name → blur commits rename", async () => {
			const user = userEvent.setup();
			renderRow();

			await user.click(screen.getByRole("button", { name: "Rename" }));
			const input = screen.getByDisplayValue("Goblin");
			await user.clear(input);
			await user.type(input, "Hobgoblin");
			await user.tab(); // blur
			// The input should be gone, name committed
			expect(screen.queryByDisplayValue("Hobgoblin")).not.toBeInTheDocument();
		});

		it("Escape cancels without renaming", async () => {
			const user = userEvent.setup();
			renderRow();

			await user.click(screen.getByRole("button", { name: "Rename" }));
			const input = screen.getByDisplayValue("Goblin");
			await user.clear(input);
			await user.type(input, "Changed");
			await user.keyboard("{Escape}");
			// Should revert to showing the original name
			expect(screen.getByText("Goblin")).toBeInTheDocument();
		});
	});

	describe("inline AC editing", () => {
		it("click AC → type value → Enter commits", async () => {
			const user = userEvent.setup();
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					ac: 13,
				},
			});

			// Click the AC shield button
			const acButton = screen.getByText("13").closest("button");
			expect(acButton).not.toBeNull();
			await user.click(acButton as HTMLElement);
			const input = screen.getByDisplayValue("13");
			await user.clear(input);
			await user.type(input, "16");
			await user.keyboard("{Enter}");
			expect(screen.queryByDisplayValue("16")).not.toBeInTheDocument();
		});
	});

	describe("inline max HP editing", () => {
		it("click max HP → type value → blur commits", async () => {
			const user = userEvent.setup();
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					maxHp: 10,
					currentHp: 10,
				},
			});

			// The max HP button shows "10" as muted text
			const maxHpButton = screen
				.getAllByText("10")
				.find(
					(el) => el.closest("button") && el.className.includes("text-muted"),
				);
			expect(maxHpButton).toBeDefined();
			const maxHpBtn = (maxHpButton as HTMLElement).closest("button");
			expect(maxHpBtn).not.toBeNull();
			await user.click(maxHpBtn as HTMLElement);
			const input = screen.getByDisplayValue("10");
			await user.clear(input);
			await user.type(input, "25");
			await user.tab();
			expect(screen.queryByDisplayValue("25")).not.toBeInTheDocument();
		});
	});

	describe("inline initiative editing", () => {
		it("click initiative → type value → Enter commits", async () => {
			const user = userEvent.setup();
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					initiative: 15,
				},
			});

			await user.click(screen.getByText("15"));
			const input = screen.getByDisplayValue("15");
			await user.clear(input);
			await user.type(input, "20");
			await user.keyboard("{Enter}");
			expect(screen.queryByDisplayValue("20")).not.toBeInTheDocument();
		});

		it("clearing initiative and pressing Enter commits the edit", async () => {
			const user = userEvent.setup();
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					initiative: 15,
				},
			});

			await user.click(screen.getByText("15"));
			const input = screen.getByDisplayValue("15");
			await user.clear(input);
			await user.keyboard("{Enter}");
			// Input should be dismissed (editing mode exited)
			expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
		});
	});

	describe("HP popover", () => {
		it("clicking current HP opens the HP adjust popover", async () => {
			const user = userEvent.setup();
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					maxHp: 10,
					currentHp: 7,
				},
			});

			const hpButton = screen.getByLabelText(CURRENT_HP_7_REGEX);
			await user.click(hpButton);
			// The popover should appear with damage/heal controls
			expect(
				screen.getByRole("button", { name: "Apply damage" }),
			).toBeInTheDocument();
			expect(
				screen.getByRole("button", { name: "Apply healing" }),
			).toBeInTheDocument();
		});

		it("HP section is absent when maxHp is undefined", () => {
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
				},
			});
			expect(screen.queryByLabelText(CURRENT_HP_REGEX)).not.toBeInTheDocument();
		});
	});

	describe("condition picker", () => {
		it("clicking Add condition button opens the picker", async () => {
			const user = userEvent.setup();
			renderRow();
			const addButton = screen.getByRole("button", {
				name: "Add condition",
			});
			await user.click(addButton);
			// Condition picker should render with condition options
			expect(screen.getByText("Blinded")).toBeInTheDocument();
		});
	});

	describe("temp HP display", () => {
		it("shows +N when combatant has temp HP", () => {
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					maxHp: 20,
					currentHp: 15,
					tempHp: 5,
				},
			});
			expect(screen.getByText("+5")).toBeInTheDocument();
		});

		it("does not show +N when combatant has no temp HP", () => {
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					maxHp: 20,
					currentHp: 15,
				},
			});
			expect(screen.queryByText(TEMP_HP_REGEX)).not.toBeInTheDocument();
		});

		it("temp HP display uses cyan color", () => {
			renderRow({
				combatant: {
					id: combatantId("1"),
					name: "Goblin",
					maxHp: 20,
					currentHp: 15,
					tempHp: 8,
				},
			});
			const tempHpEl = screen.getByText("+8");
			expect(tempHpEl.className).toContain("text-cyan-400");
		});
	});
});
