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

import { playerCharacterId } from "@initiative/domain";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactNode } from "react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js";
import { polyfillDialog } from "../../__tests__/polyfill-dialog.js";
import { AllProviders } from "../../__tests__/test-providers.js";
import { ActionBar } from "../action-bar.js";

// DOM API stubs — jsdom doesn't implement these
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(),
		})),
	});
	polyfillDialog();
});

afterEach(cleanup);

function renderBar(props: Partial<Parameters<typeof ActionBar>[0]> = {}) {
	return render(<ActionBar {...props} />, { wrapper: AllProviders });
}

function renderBarWithBestiary(
	props: Partial<Parameters<typeof ActionBar>[0]> = {},
) {
	const adapters = createTestAdapters();
	adapters.bestiaryIndex = {
		...adapters.bestiaryIndex,
		loadIndex: () => ({
			sources: { MM: "Monster Manual" },
			creatures: [
				{
					name: "Goblin",
					source: "MM",
					ac: 15,
					hp: 7,
					dex: 14,
					cr: "1/4",
					initiativeProficiency: 0,
					size: "Small",
					type: "humanoid",
				},
				{
					name: "Golem, Iron",
					source: "MM",
					ac: 20,
					hp: 210,
					dex: 9,
					cr: "16",
					initiativeProficiency: 0,
					size: "Large",
					type: "construct",
				},
			],
		}),
		getSourceDisplayName: (code: string) =>
			code === "MM" ? "Monster Manual" : code,
	};
	return render(<ActionBar {...props} />, {
		wrapper: ({ children }: { children: ReactNode }) => (
			<AllProviders adapters={adapters}>{children}</AllProviders>
		),
	});
}

function renderBarWithPCs(
	props: Partial<Parameters<typeof ActionBar>[0]> = {},
) {
	const adapters = createTestAdapters({
		playerCharacters: [
			{
				id: playerCharacterId("pc-1"),
				name: "Gandalf",
				ac: 15,
				maxHp: 40,
			},
		],
	});
	adapters.bestiaryIndex = {
		...adapters.bestiaryIndex,
		loadIndex: () => ({
			sources: { MM: "Monster Manual" },
			creatures: [
				{
					name: "Goblin",
					source: "MM",
					ac: 15,
					hp: 7,
					dex: 14,
					cr: "1/4",
					initiativeProficiency: 0,
					size: "Small",
					type: "humanoid",
				},
			],
		}),
		getSourceDisplayName: (code: string) =>
			code === "MM" ? "Monster Manual" : code,
	};
	return render(<ActionBar {...props} />, {
		wrapper: ({ children }: { children: ReactNode }) => (
			<AllProviders adapters={adapters}>{children}</AllProviders>
		),
	});
}

describe("ActionBar", () => {
	describe("basic rendering and custom add", () => {
		it("renders input with placeholder '+ Add combatants'", () => {
			renderBar();
			expect(
				screen.getByPlaceholderText("+ Add combatants"),
			).toBeInTheDocument();
		});

		it("submitting with a name adds a combatant", async () => {
			const user = userEvent.setup();
			renderBar();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Goblin");
			const addButton = screen.getByRole("button", { name: "Add" });
			await user.click(addButton);
			expect(input).toHaveValue("");
		});

		it("submitting with empty name does nothing", async () => {
			const user = userEvent.setup();
			renderBar();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "{Enter}");
			expect(input).toHaveValue("");
		});

		it("shows custom fields (Init, AC, MaxHP) when name >= 2 chars and no bestiary suggestions", async () => {
			const user = userEvent.setup();
			renderBar();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");
			expect(screen.getByPlaceholderText("Init")).toBeInTheDocument();
			expect(screen.getByPlaceholderText("AC")).toBeInTheDocument();
			expect(screen.getByPlaceholderText("MaxHP")).toBeInTheDocument();
		});

		it("shows Add button when name >= 2 chars and no suggestions", async () => {
			const user = userEvent.setup();
			renderBar();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");
			expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
		});

		it("submits custom stats with combatant", async () => {
			const user = userEvent.setup();
			renderBar();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Fighter");
			await user.type(screen.getByPlaceholderText("Init"), "15");
			await user.type(screen.getByPlaceholderText("AC"), "18");
			await user.type(screen.getByPlaceholderText("MaxHP"), "45");
			await user.click(screen.getByRole("button", { name: "Add" }));
			expect(input).toHaveValue("");
		});
	});

	describe("bestiary suggestions and queuing", () => {
		it("shows bestiary suggestions when typing a matching name", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");

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

		it("clicking a suggestion queues it with count badge", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");

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

			// Click the Goblin suggestion
			await user.click(screen.getByText("Goblin"));

			// Should show count badge "1"
			expect(screen.getByText("1")).toBeInTheDocument();
		});

		it("clicking same suggestion again increments count", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");

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

			await user.click(screen.getByText("Goblin"));
			await user.click(screen.getByText("Goblin"));

			expect(screen.getByText("2")).toBeInTheDocument();
		});

		it("confirming queued creatures adds them to the encounter", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");

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

			// Queue 1 Goblin
			await user.click(screen.getByText("Goblin"));

			// Press Enter to confirm the queued creature
			await user.keyboard("{Enter}");

			// Input should be cleared after confirming
			expect(input).toHaveValue("");
		});

		it("clears queued when search text no longer matches", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Go");

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

			await user.click(screen.getByText("Goblin"));
			expect(screen.getByText("1")).toBeInTheDocument();

			// Change search to something with no matches
			await user.clear(input);
			await user.type(input, "xyz");

			// Count badge should be gone
			expect(screen.queryByText("1")).not.toBeInTheDocument();
		});
	});

	describe("player character matching", () => {
		it("shows matching player characters in suggestions", async () => {
			const user = userEvent.setup();
			renderBarWithPCs();
			const input = screen.getByPlaceholderText("+ Add combatants");
			await user.type(input, "Gan");

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

	describe("browse mode", () => {
		it("toggles browse mode via eye icon button", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();

			const browseButton = screen.getByRole("button", {
				name: "Browse stat blocks",
			});
			await user.click(browseButton);

			expect(
				screen.getByPlaceholderText("Search stat blocks..."),
			).toBeInTheDocument();
			expect(
				screen.getByRole("button", { name: "Switch to add mode" }),
			).toBeInTheDocument();
		});

		it("browse mode shows suggestions without add UI", async () => {
			const user = userEvent.setup();
			renderBarWithBestiary();

			await user.click(
				screen.getByRole("button", { name: "Browse stat blocks" }),
			);
			const input = screen.getByPlaceholderText("Search stat blocks...");
			await user.type(input, "Go");

			await waitFor(() => {
				expect(screen.getByText("Goblin")).toBeInTheDocument();
			});
			// No Add button in browse mode
			expect(
				screen.queryByRole("button", { name: "Add" }),
			).not.toBeInTheDocument();
		});
	});

	describe("overflow menu", () => {
		it("does not show roll all initiative button when no creature combatants", () => {
			renderBar();
			expect(
				screen.queryByRole("button", { name: "Roll all initiative" }),
			).not.toBeInTheDocument();
		});

		it("shows overflow menu items", () => {
			renderBar({ onManagePlayers: vi.fn() });
			expect(
				screen.getByRole("button", { name: "More actions" }),
			).toBeInTheDocument();
		});

		it("opens export method dialog via overflow menu", async () => {
			const user = userEvent.setup();
			renderBar();
			await user.click(screen.getByRole("button", { name: "More actions" }));
			const items = screen.getAllByText("Export Encounter");
			await user.click(items[0]);
			expect(
				screen.getAllByText("Export Encounter").length,
			).toBeGreaterThanOrEqual(1);
		});

		it("opens import method dialog via overflow menu", async () => {
			const user = userEvent.setup();
			renderBar();
			await user.click(screen.getByRole("button", { name: "More actions" }));
			const items = screen.getAllByText("Import Encounter");
			await user.click(items[0]);
			expect(
				screen.getAllByText("Import Encounter").length,
			).toBeGreaterThanOrEqual(1);
		});

		it("calls onManagePlayers from overflow menu", async () => {
			const onManagePlayers = vi.fn();
			const user = userEvent.setup();
			renderBar({ onManagePlayers });
			await user.click(screen.getByRole("button", { name: "More actions" }));
			await user.click(screen.getByText("Player Characters"));
			expect(onManagePlayers).toHaveBeenCalledOnce();
		});

		it("calls onOpenSettings from overflow menu", async () => {
			const onOpenSettings = vi.fn();
			const user = userEvent.setup();
			renderBar({ onOpenSettings });
			await user.click(screen.getByRole("button", { name: "More actions" }));
			await user.click(screen.getByText("Settings"));
			expect(onOpenSettings).toHaveBeenCalledOnce();
		});
	});
});
