// @vitest-environment jsdom
import type {
	AnyCreature,
	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,
	buildPf2eCreature,
} from "../../__tests__/factories/index.js";
import { AllProviders } from "../../__tests__/test-providers.js";
import { useDifficulty } from "../use-difficulty.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 pcId2 = playerCharacterId("pc-2");
const crId1 = creatureId("srd:goblin");

const goblinCreature = buildCreature({
	id: crId1,
	name: "Goblin",
	cr: "1/4",
});

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

describe("useDifficulty", () => {
	it("returns difficulty result for leveled PCs and bestiary monsters", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "Goblin",
						creatureId: crId1,
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 1 },
			],
			creatures: new Map([[crId1, goblinCreature]]),
		});

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

		await waitFor(() => {
			expect(result.current).not.toBeNull();
			expect(result.current?.tier).toBe(1);
			expect(result.current?.totalMonsterXp).toBe(50);
		});
	});

	describe("returns null when data is insufficient (ED-2)", () => {
		it("returns null when encounter has no combatants", () => {
			const wrapper = makeWrapper({
				encounter: buildEncounter({ combatants: [] }),
			});

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

		it("returns null when only custom combatants (no creatureId)", () => {
			const wrapper = makeWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Custom",
							playerCharacterId: pcId1,
						}),
					],
				}),
				playerCharacters: [
					{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
				],
			});

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

		it("returns null when bestiary monsters present but no PC combatants", () => {
			const wrapper = makeWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Goblin",
							creatureId: crId1,
						}),
					],
				}),
				creatures: new Map([[crId1, goblinCreature]]),
			});

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

		it("returns null when PC combatants have no level", () => {
			const wrapper = makeWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Hero",
							playerCharacterId: pcId1,
						}),
						buildCombatant({
							id: combatantId("c2"),
							name: "Goblin",
							creatureId: crId1,
						}),
					],
				}),
				playerCharacters: [{ id: pcId1, name: "Hero", ac: 15, maxHp: 30 }],
				creatures: new Map([[crId1, goblinCreature]]),
			});

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

		it("returns null when PC combatant references unknown player character", () => {
			const wrapper = makeWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Hero",
							playerCharacterId: pcId2,
						}),
						buildCombatant({
							id: combatantId("c2"),
							name: "Goblin",
							creatureId: crId1,
						}),
					],
				}),
				playerCharacters: [
					{ id: pcId1, name: "Other", ac: 15, maxHp: 30, level: 5 },
				],
				creatures: new Map([[crId1, goblinCreature]]),
			});

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

	it("handles mixed combatants: only leveled PCs and CR-bearing monsters contribute", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Leveled",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "No Level",
						playerCharacterId: pcId2,
					}),
					buildCombatant({
						id: combatantId("c3"),
						name: "Goblin",
						creatureId: crId1,
					}),
					buildCombatant({
						id: combatantId("c4"),
						name: "Custom Monster",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Leveled", ac: 15, maxHp: 30, level: 1 },
				{ id: pcId2, name: "No Level", ac: 12, maxHp: 20 },
			],
			creatures: new Map([[crId1, goblinCreature]]),
		});

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

		await waitFor(() => {
			expect(result.current).not.toBeNull();
			// 1 level-1 PC: budget low=50, mod=75, high=100
			// CR 1/4 = 50 XP -> low (50 >= 50)
			expect(result.current?.tier).toBe(1);
			expect(result.current?.totalMonsterXp).toBe(50);
			expect(result.current?.thresholds[0].value).toBe(50);
		});
	});

	it("includes duplicate PC combatants in budget", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Hero 1",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "Hero 2",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c3"),
						name: "Goblin",
						creatureId: crId1,
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 1 },
			],
			creatures: new Map([[crId1, goblinCreature]]),
		});

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

		await waitFor(() => {
			expect(result.current).not.toBeNull();
			// 2x level 1: budget low=100
			expect(result.current?.thresholds[0].value).toBe(100);
		});
	});

	it("combatant toggled to party side subtracts XP", async () => {
		const bugbear = buildCreature({
			id: creatureId("srd:bugbear"),
			name: "Bugbear",
			cr: "1",
		});
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "Allied Guard",
						creatureId: bugbear.id,
						side: "party",
					}),
					buildCombatant({
						id: combatantId("c3"),
						name: "Thug",
						cr: "1",
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 1 },
			],
			creatures: new Map([[bugbear.id, bugbear]]),
		});

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

		await waitFor(() => {
			expect(result.current).not.toBeNull();
			// Thug CR 1 = 200 XP, Allied Guard CR 1 = 200 XP subtracted, net = 0
			expect(result.current?.totalMonsterXp).toBe(0);
			expect(result.current?.tier).toBe(0);
		});
	});

	it("default side resolution: PC -> party, non-PC -> enemy", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "Goblin",
						creatureId: crId1,
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 3 },
			],
			creatures: new Map([[crId1, goblinCreature]]),
		});

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

		await waitFor(() => {
			expect(result.current).not.toBeNull();
			// Level 3 budget: low=150, mod=225, high=400
			// CR 1/4 = 50 XP -> trivial
			expect(result.current?.thresholds[0].value).toBe(150);
			expect(result.current?.totalMonsterXp).toBe(50);
			expect(result.current?.tier).toBe(0);
		});
	});

	it("returns 2014 difficulty when edition is 5e", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "Goblin",
						creatureId: crId1,
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 1 },
			],
			creatures: new Map([[crId1, goblinCreature]]),
		});

		// Set edition via the hook's external store
		const { result: editionResult } = renderHook(() => useRulesEdition(), {
			wrapper,
		});
		editionResult.current.setEdition("5e");

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

			await waitFor(() => {
				expect(result.current).not.toBeNull();
				// 2014: 4 thresholds with Easy/Medium/Hard/Deadly labels
				expect(result.current?.thresholds).toHaveLength(4);
				expect(result.current?.thresholds[0].label).toBe("Easy");
				// CR 1/4 = 50 XP, 1 PC (<3) shifts x1 → x1.5, adjusted = 75
				expect(result.current?.encounterMultiplier).toBe(1.5);
				expect(result.current?.adjustedXp).toBe(75);
			});
		} finally {
			editionResult.current.setEdition("5.5e");
		}
	});

	it("custom combatant with CR on party side subtracts XP", async () => {
		const wrapper = makeWrapper({
			encounter: buildEncounter({
				combatants: [
					buildCombatant({
						id: combatantId("c1"),
						name: "Hero",
						playerCharacterId: pcId1,
					}),
					buildCombatant({
						id: combatantId("c2"),
						name: "Ally",
						cr: "2",
						side: "party",
					}),
					buildCombatant({
						id: combatantId("c3"),
						name: "Goblin",
						creatureId: crId1,
					}),
				],
			}),
			playerCharacters: [
				{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
			],
			creatures: new Map([[crId1, goblinCreature]]),
		});

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

		await waitFor(() => {
			expect(result.current).not.toBeNull();
			// CR 1/4 = 50 XP enemy, CR 2 = 450 XP ally subtracted, net = 0 (floored)
			expect(result.current?.totalMonsterXp).toBe(0);
		});
	});

	describe("PF2e edition", () => {
		const pf2eCreature = buildPf2eCreature({
			id: creatureId("pf2e:orc-warrior"),
			name: "Orc Warrior",
			level: 5,
		});

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

		it("returns result for PF2e with leveled PCs and PF2e creatures", async () => {
			const wrapper = makePf2eWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Hero",
							playerCharacterId: pcId1,
						}),
						buildCombatant({
							id: combatantId("c2"),
							name: "Orc Warrior",
							creatureId: pf2eCreature.id,
						}),
					],
				}),
				playerCharacters: [
					{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
				],
				creatures: new Map([[pf2eCreature.id, pf2eCreature]]),
			});

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

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

				await waitFor(() => {
					expect(result.current).not.toBeNull();
					// Creature level 5, party level 5 → diff 0 → 40 XP
					expect(result.current?.totalMonsterXp).toBe(40);
					expect(result.current?.partyLevel).toBe(5);
				});
			} finally {
				editionResult.current.setEdition("5.5e");
			}
		});

		it("returns null for PF2e when no PF2e creatures with level", () => {
			const wrapper = makePf2eWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Hero",
							playerCharacterId: pcId1,
						}),
						buildCombatant({
							id: combatantId("c2"),
							name: "Custom Monster",
						}),
					],
				}),
				playerCharacters: [
					{ id: pcId1, name: "Hero", ac: 15, maxHp: 30, level: 5 },
				],
			});

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

			try {
				const { result } = renderHook(() => useDifficulty(), { wrapper });
				expect(result.current).toBeNull();
			} finally {
				editionResult.current.setEdition("5.5e");
			}
		});

		it("returns null for PF2e when no PCs with level", () => {
			const wrapper = makePf2eWrapper({
				encounter: buildEncounter({
					combatants: [
						buildCombatant({
							id: combatantId("c1"),
							name: "Hero",
							playerCharacterId: pcId1,
						}),
						buildCombatant({
							id: combatantId("c2"),
							name: "Orc Warrior",
							creatureId: pf2eCreature.id,
						}),
					],
				}),
				playerCharacters: [{ id: pcId1, name: "Hero", ac: 15, maxHp: 30 }],
				creatures: new Map([[pf2eCreature.id, pf2eCreature]]),
			});

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

			try {
				const { result } = renderHook(() => useDifficulty(), { wrapper });
				expect(result.current).toBeNull();
			} finally {
				editionResult.current.setEdition("5.5e");
			}
		});
	});
});
