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

import type { Creature, CreatureId } from "@initiative/domain";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

// Mock the context modules
vi.mock("../contexts/side-panel-context.js", () => ({
	useSidePanelContext: vi.fn(),
}));

vi.mock("../contexts/bestiary-context.js", () => ({
	useBestiaryContext: vi.fn(),
}));

// Mock adapters to avoid IndexedDB
vi.mock("../adapters/bestiary-index-adapter.js", () => ({
	loadBestiaryIndex: () => ({ sources: {}, creatures: [] }),
	getAllSourceCodes: () => [],
	getDefaultFetchUrl: () => "",
	getSourceDisplayName: (code: string) => code,
}));

import { StatBlockPanel } from "../components/stat-block-panel.js";
import { useBestiaryContext } from "../contexts/bestiary-context.js";
import { useSidePanelContext } from "../contexts/side-panel-context.js";

const mockUseSidePanelContext = vi.mocked(useSidePanelContext);
const mockUseBestiaryContext = vi.mocked(useBestiaryContext);

const CLOSE_REGEX = /close/i;
const COLLAPSE_REGEX = /collapse/i;
const CREATURE_ID = "srd:goblin" as CreatureId;
const CREATURE: Creature = {
	id: CREATURE_ID,
	name: "Goblin",
	source: "SRD",
	sourceDisplayName: "SRD",
	size: "Small",
	type: "humanoid",
	alignment: "neutral evil",
	ac: 15,
	hp: { average: 7, formula: "2d6" },
	speed: "30 ft.",
	abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 },
	cr: "1/4",
	initiativeProficiency: 0,
	proficiencyBonus: 2,
	passive: 9,
};

function mockMatchMedia(matches: boolean) {
	Object.defineProperty(globalThis, "matchMedia", {
		writable: true,
		value: vi.fn().mockImplementation((query: string) => ({
			matches,
			media: query,
			onchange: null,
			addListener: vi.fn(),
			removeListener: vi.fn(),
			addEventListener: vi.fn(),
			removeEventListener: vi.fn(),
			dispatchEvent: vi.fn(),
		})),
	});
}

interface PanelOverrides {
	creatureId?: CreatureId | null;
	creature?: Creature | null;
	panelRole?: "browse" | "pinned";
	isCollapsed?: boolean;
	side?: "left" | "right";
	bulkImportMode?: boolean;
}

function setupMocks(overrides: PanelOverrides = {}) {
	const panelRole = overrides.panelRole ?? "browse";
	const creatureId = overrides.creatureId ?? CREATURE_ID;
	const creature = overrides.creature ?? CREATURE;
	const isCollapsed = overrides.isCollapsed ?? false;

	const onToggleCollapse = vi.fn();
	const onPin = vi.fn();
	const onUnpin = vi.fn();
	const onDismiss = vi.fn();

	mockUseSidePanelContext.mockReturnValue({
		selectedCreatureId: panelRole === "browse" ? creatureId : null,
		pinnedCreatureId: panelRole === "pinned" ? creatureId : null,
		isRightPanelCollapsed: panelRole === "browse" ? isCollapsed : false,
		isWideDesktop: false,
		bulkImportMode: overrides.bulkImportMode ?? false,
		sourceManagerMode: false,
		panelView: creatureId
			? { mode: "creature" as const, creatureId }
			: { mode: "closed" as const },
		showCreature: vi.fn(),
		updateCreature: vi.fn(),
		showBulkImport: vi.fn(),
		showSourceManager: vi.fn(),
		dismissPanel: onDismiss,
		toggleCollapse: onToggleCollapse,
		togglePin: onPin,
		unpin: onUnpin,
	} as ReturnType<typeof useSidePanelContext>);

	mockUseBestiaryContext.mockReturnValue({
		getCreature: (id: CreatureId) => (id === creatureId ? creature : undefined),
		isSourceCached: vi.fn().mockResolvedValue(true),
		search: vi.fn().mockReturnValue([]),
		isLoaded: true,
		fetchAndCacheSource: vi.fn(),
		uploadAndCacheSource: vi.fn(),
		refreshCache: vi.fn(),
	} as ReturnType<typeof useBestiaryContext>);

	return { onToggleCollapse, onPin, onUnpin, onDismiss };
}

function renderPanel(overrides: PanelOverrides = {}) {
	const callbacks = setupMocks(overrides);
	const panelRole = overrides.panelRole ?? "browse";
	const side = overrides.side ?? (panelRole === "pinned" ? "left" : "right");
	render(<StatBlockPanel panelRole={panelRole} side={side} />);
	return callbacks;
}

describe("Stat Block Panel Collapse/Expand and Pin", () => {
	beforeEach(() => {
		mockMatchMedia(true); // desktop by default
	});

	afterEach(cleanup);

	describe("US1: Collapse and Expand", () => {
		it("shows collapse button instead of close button on desktop", () => {
			renderPanel();
			expect(
				screen.getByRole("button", { name: "Collapse stat block panel" }),
			).toBeInTheDocument();
			expect(
				screen.queryByRole("button", { name: CLOSE_REGEX }),
			).not.toBeInTheDocument();
		});

		it("does not show 'Stat Block' heading", () => {
			renderPanel();
			expect(screen.queryByText("Stat Block")).not.toBeInTheDocument();
		});

		it("renders collapsed tab with creature name when isCollapsed is true", () => {
			renderPanel({ isCollapsed: true });
			expect(screen.getByText("Goblin")).toBeInTheDocument();
			expect(
				screen.getByRole("button", { name: "Expand stat block panel" }),
			).toBeInTheDocument();
		});

		it("calls onToggleCollapse when collapse button is clicked", () => {
			const callbacks = renderPanel();
			fireEvent.click(
				screen.getByRole("button", { name: "Collapse stat block panel" }),
			);
			expect(callbacks.onToggleCollapse).toHaveBeenCalledTimes(1);
		});

		it("calls onToggleCollapse when collapsed tab is clicked", () => {
			const callbacks = renderPanel({ isCollapsed: true });
			fireEvent.click(
				screen.getByRole("button", { name: "Expand stat block panel" }),
			);
			expect(callbacks.onToggleCollapse).toHaveBeenCalledTimes(1);
		});

		it("applies translate-x class when collapsed (right side)", () => {
			renderPanel({ isCollapsed: true, side: "right" });
			const panel = screen
				.getByRole("button", { name: "Expand stat block panel" })
				.closest("div");
			expect(panel?.className).toContain("translate-x-[calc(100%-40px)]");
		});

		it("applies translate-x-0 when expanded", () => {
			renderPanel({ isCollapsed: false });
			const foldBtn = screen.getByRole("button", {
				name: "Collapse stat block panel",
			});
			const panel = foldBtn.closest("div.fixed") as HTMLElement;
			expect(panel?.className).toContain("translate-x-0");
		});
	});

	describe("US1: Mobile behavior", () => {
		beforeEach(() => {
			mockMatchMedia(false); // mobile
		});

		it("shows collapse button instead of X close button on mobile drawer", () => {
			renderPanel();
			expect(
				screen.getByRole("button", { name: "Collapse stat block panel" }),
			).toBeInTheDocument();
			// No X close icon button — only backdrop dismiss and collapse toggle
			const buttons = screen.getAllByRole("button");
			const buttonLabels = buttons.map((b) => b.getAttribute("aria-label"));
			expect(buttonLabels).not.toContain("Close");
		});

		it("calls onDismiss when backdrop is clicked on mobile", () => {
			const callbacks = renderPanel();
			fireEvent.click(screen.getByRole("button", { name: "Close stat block" }));
			expect(callbacks.onDismiss).toHaveBeenCalledTimes(1);
		});

		it("does not render pinned panel on mobile", () => {
			const { container } = render(
				(() => {
					setupMocks({ panelRole: "pinned" });
					return <StatBlockPanel panelRole="pinned" side="left" />;
				})(),
			);
			expect(container.innerHTML).toBe("");
		});
	});

	describe("US2: Pin and Unpin", () => {
		it("shows pin button when isWideDesktop is true on desktop", () => {
			setupMocks();
			// Override to set isWideDesktop
			const ctx = mockUseSidePanelContext.mock.results[0]?.value as ReturnType<
				typeof useSidePanelContext
			>;
			mockUseSidePanelContext.mockReturnValue({
				...ctx,
				isWideDesktop: true,
			});
			render(<StatBlockPanel panelRole="browse" side="right" />);
			expect(
				screen.getByRole("button", { name: "Pin creature" }),
			).toBeInTheDocument();
		});

		it("hides pin button when isWideDesktop is false", () => {
			renderPanel();
			expect(
				screen.queryByRole("button", { name: "Pin creature" }),
			).not.toBeInTheDocument();
		});

		it("calls onPin when pin button is clicked", () => {
			setupMocks();
			const ctx = mockUseSidePanelContext.mock.results[0]?.value as ReturnType<
				typeof useSidePanelContext
			>;
			mockUseSidePanelContext.mockReturnValue({
				...ctx,
				isWideDesktop: true,
			});
			render(<StatBlockPanel panelRole="browse" side="right" />);
			fireEvent.click(screen.getByRole("button", { name: "Pin creature" }));
			expect(ctx.togglePin).toHaveBeenCalledTimes(1);
		});

		it("shows unpin button for pinned role", () => {
			renderPanel({ panelRole: "pinned", side: "left" });
			expect(
				screen.getByRole("button", { name: "Unpin creature" }),
			).toBeInTheDocument();
		});

		it("calls onUnpin when unpin button is clicked", () => {
			const callbacks = renderPanel({ panelRole: "pinned", side: "left" });
			fireEvent.click(screen.getByRole("button", { name: "Unpin creature" }));
			expect(callbacks.onUnpin).toHaveBeenCalledTimes(1);
		});

		it("positions pinned panel on the left side", () => {
			renderPanel({ panelRole: "pinned", side: "left" });
			const unpinBtn = screen.getByRole("button", {
				name: "Unpin creature",
			});
			const panel = unpinBtn.closest("div.fixed") as HTMLElement;
			expect(panel?.className).toContain("left-0");
			expect(panel?.className).toContain("border-r");
		});

		it("positions browse panel on the right side", () => {
			renderPanel({ panelRole: "browse", side: "right" });
			const foldBtn = screen.getByRole("button", {
				name: "Collapse stat block panel",
			});
			const panel = foldBtn.closest("div.fixed") as HTMLElement;
			expect(panel?.className).toContain("right-0");
			expect(panel?.className).toContain("border-l");
		});
	});

	describe("US3: Collapse independence with pinned panel", () => {
		it("pinned panel has no collapse button", () => {
			renderPanel({ panelRole: "pinned", side: "left" });
			expect(
				screen.queryByRole("button", { name: COLLAPSE_REGEX }),
			).not.toBeInTheDocument();
		});

		it("pinned panel is always expanded (no translate offset)", () => {
			renderPanel({ panelRole: "pinned", side: "left" });
			const unpinBtn = screen.getByRole("button", {
				name: "Unpin creature",
			});
			const panel = unpinBtn.closest("div.fixed") as HTMLElement;
			expect(panel?.className).toContain("translate-x-0");
		});
	});
});
