// @vitest-environment jsdom
import type { Creature, CreatureId, PlayerCharacter } from "@initiative/domain";
import { combatantId, creatureId, playerCharacterId } from "@initiative/domain";
import { renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
import { 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 { useDifficultyBreakdown } from "../use-difficulty-breakdown.js";
import { useRulesEdition } from "../use-rules-edition.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(),
		})),
	});
});

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

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

describe("useDifficultyBreakdown", () => {
	it("returns null when no leveled PCs", () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c-1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c-2"),
						name: "Goblin",
						creatureId: goblinCreature.id,
					}),
				],
			}),
			playerCharacters: [{ id: pcId1, name: "Hero", ac: 15, maxHp: 30 }],
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		const { result } = renderHook(() => useDifficultyBreakdown(), {
			wrapper,
		});
		expect(result.current).toBeNull();
	});

	it("returns null when no monsters with CR", () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c-1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c-2"),
						name: "Custom",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
			],
		});

		const { result } = renderHook(() => useDifficultyBreakdown(), {
			wrapper,
		});
		expect(result.current).toBeNull();
	});

	it("returns per-combatant entries split by side", async () => {
		const wrapper = makeWrapper({
			encounter: 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",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
			],
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		const { result } = renderHook(() => useDifficultyBreakdown(), {
			wrapper,
		});

		await waitFor(() => {
			const breakdown = result.current;
			expect(breakdown).not.toBeNull();
			expect(breakdown?.pcCount).toBe(1);
			// CR 1/4 = 50 + CR 2 = 450 -> total 500
			expect(breakdown?.totalMonsterXp).toBe(500);

			// PC in party column
			expect(breakdown?.partyCombatants).toHaveLength(1);
			expect(breakdown?.partyCombatants[0].combatant.name).toBe("Hero");
			expect(breakdown?.partyCombatants[0].side).toBe("party");
			expect(breakdown?.partyCombatants[0].level).toBe(5);

			// Enemies: goblin, thug, bandit
			expect(breakdown?.enemyCombatants).toHaveLength(3);

			const goblin = breakdown?.enemyCombatants[0];
			expect(goblin?.cr).toBe("1/4");
			expect(goblin?.xp).toBe(50);
			expect(goblin?.source).toBe("SRD");
			expect(goblin?.editable).toBe(false);
			expect(goblin?.side).toBe("enemy");

			const thug = breakdown?.enemyCombatants[1];
			expect(thug?.cr).toBe("2");
			expect(thug?.xp).toBe(450);
			expect(thug?.source).toBeNull();
			expect(thug?.editable).toBe(true);

			const bandit = breakdown?.enemyCombatants[2];
			expect(bandit?.cr).toBeNull();
			expect(bandit?.xp).toBeNull();
			expect(bandit?.editable).toBe(true);
		});
	});

	it("bestiary combatant with missing creature is non-editable with null CR", () => {
		const missingCreatureId = creatureId("creature-missing");
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c-1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c-2"),
						name: "Ghost",
						creatureId: missingCreatureId,
					}),
					buildCombatant({
						id: combatantId("c-3"),
						name: "Thug",
						cr: "1",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
			],
		});

		const { result } = renderHook(() => useDifficultyBreakdown(), {
			wrapper,
		});

		const breakdown = result.current;
		expect(breakdown).not.toBeNull();
		const ghost = breakdown?.enemyCombatants[0];
		expect(ghost?.cr).toBeNull();
		expect(ghost?.xp).toBeNull();
		expect(ghost?.editable).toBe(false);
	});

	it("PC combatants appear in partyCombatants with level", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c-1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c-2"),
						name: "Goblin",
						creatureId: goblinCreature.id,
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 1 },
			],
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		const { result } = renderHook(() => useDifficultyBreakdown(), {
			wrapper,
		});

		await waitFor(() => {
			expect(result.current?.partyCombatants).toHaveLength(1);
			expect(result.current?.partyCombatants[0].combatant.name).toBe("Hero");
			expect(result.current?.partyCombatants[0].level).toBe(1);
			expect(result.current?.partyCombatants[0].side).toBe("party");
		});
	});

	it("combatant with explicit side override is placed correctly", () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c-1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c-2"),
						name: "Allied Guard",
						creatureId: goblinCreature.id,
						side: "party",
					}),
					buildCombatant({
						id: combatantId("c-3"),
						name: "Thug",
						cr: "1",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
			],
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		const { result } = renderHook(() => useDifficultyBreakdown(), {
			wrapper,
		});

		const breakdown = result.current;
		expect(breakdown).not.toBeNull();
		// Allied Guard should be in party column
		expect(breakdown?.partyCombatants).toHaveLength(2);
		expect(breakdown?.partyCombatants[1].combatant.name).toBe("Allied Guard");
		expect(breakdown?.partyCombatants[1].side).toBe("party");
		// Thug in enemy column
		expect(breakdown?.enemyCombatants).toHaveLength(1);
		expect(breakdown?.enemyCombatants[0].combatant.name).toBe("Thug");
	});

	it("exposes encounterMultiplier and adjustedXp for 5e edition", async () => {
		const wrapper = makeWrapper({
			encounter: 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: "Thug",
						cr: "1",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 1 },
			],
			creatures: new Map([[goblinCreature.id, goblinCreature]]),
		});

		const { result: editionResult } = renderHook(() => useRulesEdition(), {
			wrapper,
		});
		editionResult.current.setEdition("5e");

		try {
			const { result } = renderHook(() => useDifficultyBreakdown(), {
				wrapper,
			});

			await waitFor(() => {
				const breakdown = result.current;
				expect(breakdown).not.toBeNull();
				// 2 enemy monsters, 1 PC (<3) → base x1.5, shift up → x2
				expect(breakdown?.encounterMultiplier).toBe(2);
				// CR 1/4 (50) + CR 1 (200) = 250, x2 = 500
				expect(breakdown?.totalMonsterXp).toBe(250);
				expect(breakdown?.adjustedXp).toBe(500);
				expect(breakdown?.thresholds).toHaveLength(4);
			});
		} finally {
			editionResult.current.setEdition("5.5e");
		}
	});
});
