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

import type { Creature, CreatureId, PlayerCharacter } from "@initiative/domain";
import { combatantId, creatureId, playerCharacterId } from "@initiative/domain";
import {
	cleanup,
	render,
	renderHook,
	screen,
	waitFor,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js";
import {
	buildCombatant,
	buildCreature,
	buildEncounter,
} from "../../__tests__/factories/index.js";
import { AllProviders } from "../../__tests__/test-providers.js";
import { useRulesEdition } from "../../hooks/use-rules-edition.js";
import { DifficultyBreakdownPanel } from "../difficulty-breakdown-panel.js";

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

const pcId1 = playerCharacterId("pc-1");
const goblinCreature = buildCreature({
	id: creatureId("srd:goblin"),
	name: "Goblin",
	cr: "1/4",
	source: "srd",
	sourceDisplayName: "SRD",
});

function renderPanel(options: {
	encounter: ReturnType<typeof buildEncounter>;
	playerCharacters?: PlayerCharacter[];
	creatures?: Map<CreatureId, Creature>;
	onClose?: () => void;
}) {
	const adapters = createTestAdapters({
		encounter: options.encounter,
		playerCharacters: options.playerCharacters ?? [],
		creatures: options.creatures,
	});
	return render(
		<AllProviders adapters={adapters}>
			<DifficultyBreakdownPanel onClose={options.onClose ?? (() => {})} />
		</AllProviders>,
	);
}

function defaultEncounter() {
	return buildEncounter({
		combatants: [
			buildCombatant({
				id: combatantId("c-1"),
				name: "Hero",
				playerCharacterId: pcId1,
			}),
			buildCombatant({
				id: combatantId("c-2"),
				name: "Goblin",
				creatureId: goblinCreature.id,
			}),
			buildCombatant({
				id: combatantId("c-3"),
				name: "Custom Thug",
				cr: "2",
			}),
			buildCombatant({
				id: combatantId("c-4"),
				name: "Bandit",
			}),
		],
	});
}

const defaultPCs: PlayerCharacter[] = [
	{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
];

describe("DifficultyBreakdownPanel", () => {
	it("renders party budget section", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(
				screen.getByText("Party Budget", { exact: false }),
			).toBeInTheDocument();
			expect(screen.getByText("1 PC", { exact: false })).toBeInTheDocument();
			expect(screen.getByText("Low:", { exact: false })).toBeInTheDocument();
		});
	});

	it("renders tier label", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(
				screen.getByText("Encounter Difficulty:", { exact: false }),
			).toBeInTheDocument();
		});
	});

	it("shows PC in party column with level", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getByText("Hero")).toBeInTheDocument();
			expect(screen.getByText("Lv 5")).toBeInTheDocument();
		});
	});

	it("shows monsters in enemy column", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getByText("Goblin")).toBeInTheDocument();
			expect(screen.getAllByText("CR 1/4").length).toBeGreaterThanOrEqual(1);
		});
	});

	it("renders explanation text", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(
				screen.getByText(
					"Allied NPC XP is subtracted from encounter difficulty",
				),
			).toBeInTheDocument();
		});
	});

	it("renders Net Monster XP footer", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getByText("Net Monster XP")).toBeInTheDocument();
		});
	});

	it("renders custom combatant with CR picker in enemy column", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			const pickers = screen.getAllByLabelText("Challenge rating");
			expect(pickers).toHaveLength(2);
			expect(pickers[0]).toHaveValue("2");
		});
	});

	it("selecting a CR updates the visible XP value", async () => {
		const user = userEvent.setup();
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getAllByLabelText("Challenge rating")).toHaveLength(2);
		});

		const pickers = screen.getAllByLabelText("Challenge rating");
		await user.selectOptions(pickers[1], "5");

		await waitFor(() => {
			expect(screen.getByText("1,800")).toBeInTheDocument();
		});
	});

	it("non-PC combatants show toggle button", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			// Each non-PC enemy combatant has a toggle button
			expect(
				screen.getByLabelText("Move Goblin to party side"),
			).toBeInTheDocument();
			expect(
				screen.getByLabelText("Move Custom Thug to party side"),
			).toBeInTheDocument();
		});
	});

	it("PC combatants do not show side toggle", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getByText("Hero")).toBeInTheDocument();
		});

		expect(
			screen.queryByLabelText("Move Hero to enemy side"),
		).not.toBeInTheDocument();
	});

	it("side toggle moves combatant between sections", async () => {
		const user = userEvent.setup();
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getByText("Goblin")).toBeInTheDocument();
		});

		// Toggle goblin to party side
		const toggleBtn = screen.getByLabelText("Move Goblin to party side");
		await user.click(toggleBtn);

		// After toggle, the aria-label should change to "Move Goblin to enemy side"
		await waitFor(() => {
			expect(
				screen.getByLabelText("Move Goblin to enemy side"),
			).toBeInTheDocument();
		});
	});

	it("renders nothing when breakdown data is insufficient", () => {
		const { container } = renderPanel({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({ id: combatantId("c-1"), name: "Custom" }),
				],
			}),
		});

		expect(container.innerHTML).toBe("");
	});

	it("shows 4 threshold columns for 2014 edition", async () => {
		const { result: editionResult } = renderHook(() => useRulesEdition());
		editionResult.current.setEdition("5e");

		try {
			renderPanel({
				encounter: defaultEncounter(),
				playerCharacters: defaultPCs,
				creatures: new Map([[goblinCreature.id, goblinCreature]]),
			});

			await waitFor(() => {
				expect(screen.getByText("Easy:", { exact: false })).toBeInTheDocument();
				expect(screen.getByText("Med:", { exact: false })).toBeInTheDocument();
				expect(screen.getByText("Hard:", { exact: false })).toBeInTheDocument();
				expect(
					screen.getByText("Deadly:", { exact: false }),
				).toBeInTheDocument();
			});
		} finally {
			editionResult.current.setEdition("5.5e");
		}
	});

	it("shows multiplier and adjusted XP for 2014 edition", async () => {
		const { result: editionResult } = renderHook(() => useRulesEdition());
		editionResult.current.setEdition("5e");

		try {
			renderPanel({
				encounter: defaultEncounter(),
				playerCharacters: defaultPCs,
				creatures: new Map([[goblinCreature.id, goblinCreature]]),
			});

			await waitFor(() => {
				expect(screen.getByText("Monster XP")).toBeInTheDocument();
				// 1 PC (<3) triggers party size adjustment
				expect(screen.getByText("Adjusted for 1 PC")).toBeInTheDocument();
			});
		} finally {
			editionResult.current.setEdition("5.5e");
		}
	});

	it("shows Net Monster XP for 5.5e edition (no multiplier)", async () => {
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		await waitFor(() => {
			expect(screen.getByText("Net Monster XP")).toBeInTheDocument();
		});
	});

	it("calls onClose when Escape is pressed", async () => {
		const user = userEvent.setup();
		const onClose = vi.fn();
		renderPanel({
			encounter: defaultEncounter(),
			playerCharacters: defaultPCs,
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
			onClose,
		});

		await user.keyboard("{Escape}");

		expect(onClose).toHaveBeenCalledOnce();
	});
});
