import { afterEach, beforeEach, expect, mock } from "bun:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { CalendarEvent } from "@/lib/types";

export type AIToolbarProps = {
	adminAiEnabled: boolean;
	aiEnabled: boolean;
	isAuthenticated: boolean;
	isPending: boolean;
	aiPrompt: string;
	setAiPrompt: (prompt: string) => void;
	aiLoading: boolean;
	imagePreviews: string[];
	onImagesSelect: (files: File[]) => void;
	onImageRemove: (index: number) => void;
	onAiCreate: () => void;
	onAiTemplateSelect: (prompt: string) => void;
	onAiSummarize: () => void;
	onSummaryDismiss: () => void;
	summary: string | null;
	summaryUpdated: string | null;
	events: CalendarEvent[];
};

const actualReact = React;
const documentAddEventListener = mock();
const documentRemoveEventListener = mock();
let lastEffectCleanup: (() => void) | undefined;
const registeredDocumentListeners = new Map<
	string,
	Array<{ listener: EventListener; options?: AddEventListenerOptions }>
>();

export const createToolbarProps = (
	overrides: Partial<AIToolbarProps> = {},
): AIToolbarProps => ({
	adminAiEnabled: true,
	aiEnabled: true,
	isAuthenticated: true,
	isPending: false,
	aiPrompt: "",
	setAiPrompt: () => {},
	aiLoading: false,
	imagePreviews: [],
	onImagesSelect: () => {},
	onImageRemove: () => {},
	onAiCreate: () => {},
	onAiTemplateSelect: () => {},
	onAiSummarize: () => {},
	onSummaryDismiss: () => {},
	summary: null,
	summaryUpdated: null,
	events: [],
	...overrides,
});

export const registerAIToolbarEffectHooks = () => {
	beforeEach(() => {
		documentAddEventListener.mockClear();
		documentRemoveEventListener.mockClear();
		lastEffectCleanup = undefined;
		registeredDocumentListeners.clear();
		globalThis.document = {
			addEventListener: documentAddEventListener,
			removeEventListener: documentRemoveEventListener,
		} as unknown as Document;
		documentAddEventListener.mockImplementation(
			(
				type: string,
				listener: EventListener,
				options?: AddEventListenerOptions,
			) => {
				const listeners = registeredDocumentListeners.get(type) ?? [];
				listeners.push({ listener, options });
				registeredDocumentListeners.set(type, listeners);
			},
		);
		mock.module("react", () => ({
			...actualReact,
			useEffect: (effect: () => void | (() => void)) => {
				const cleanup = effect();
				lastEffectCleanup = typeof cleanup === "function" ? cleanup : undefined;
			},
			useRef: <T,>(initialValue: T) => ({ current: initialValue }),
			useState: <T,>(initialValue: T) => [initialValue, mock()],
		}));
		mock.module("@/hooks/use-mobile", () => ({
			useIsMobile: () => false,
		}));
	});

	afterEach(() => {
		delete (globalThis as { document?: Document }).document;
		delete (globalThis as { navigator?: Navigator }).navigator;
		lastEffectCleanup = undefined;
		mock.restore();
	});
};

export const registerAIToolbarMarkupHooks = () => {
	beforeEach(() => {
		globalThis.document = {
			addEventListener: () => {},
			removeEventListener: () => {},
		} as unknown as Document;
		mock.module("@/hooks/use-mobile", () => ({
			useIsMobile: () => false,
		}));
	});

	afterEach(() => {
		delete (globalThis as { document?: Document }).document;
		delete (globalThis as { navigator?: Navigator }).navigator;
		mock.restore();
	});
};

export const registerAIToolbarTestHooks = registerAIToolbarEffectHooks;

export const renderToolbar = async (
	overrides: Partial<AIToolbarProps> = {},
) => {
	const { AIToolbar } = await import("@/components/ai-toolbar");

	AIToolbar(createToolbarProps(overrides));
};

export const renderToolbarMarkup = async (
	overrides: Partial<AIToolbarProps> = {},
	{ isMobile = false }: { isMobile?: boolean } = {},
) => {
	mock.module("@/hooks/use-mobile", () => ({
		useIsMobile: () => isMobile,
	}));

	const { AIToolbar } = await import("@/components/ai-toolbar");

	return renderToStaticMarkup(
		actualReact.createElement(AIToolbar, createToolbarProps(overrides)),
	);
};

export const renderToolbarBoundary = async (
	overrides: Partial<AIToolbarProps> = {},
) => {
	let capturedTextareaProps: React.ComponentProps<"textarea"> | undefined;
	const imageTriggerOpen = mock();

	mock.module("@/components/ui/textarea", () => ({
		Textarea: (props: React.ComponentProps<"textarea">) => {
			capturedTextareaProps = props;
			return actualReact.createElement("textarea", props);
		},
	}));
	mock.module("@/components/image-picker", () => ({
		ImagePicker: ({
			triggerRef,
			children,
			...props
		}: React.ComponentProps<"button"> & {
			triggerRef?: { current: { open: () => void } | null };
		}) => {
			if (triggerRef) {
				triggerRef.current = { open: imageTriggerOpen };
			}
			return actualReact.createElement("button", props, children);
		},
	}));

	const { AIToolbar } = await import("@/components/ai-toolbar");

	renderToStaticMarkup(
		actualReact.createElement(AIToolbar, createToolbarProps(overrides)),
	);

	expect(capturedTextareaProps).toBeDefined();

	return { textareaProps: capturedTextareaProps!, imageTriggerOpen };
};

export const renderToolbarActionBoundary = async (
	overrides: Partial<AIToolbarProps> = {},
) => {
	type CapturedButton = React.ComponentProps<"button"> & {
		label: string;
		ariaLabel?: string;
	};
	const buttons: CapturedButton[] = [];

	mock.module("@/components/ui/button", () => ({
		Button: ({ children, ...props }: React.ComponentProps<"button">) => {
			const label = renderToStaticMarkup(
				actualReact.createElement(actualReact.Fragment, null, children),
			)
				.replace(/<[^>]+>/g, "")
				.trim();
			buttons.push({
				...props,
				label,
				ariaLabel: props["aria-label"] as string | undefined,
			});
			return actualReact.createElement("button", props, children);
		},
	}));

	const { AIToolbar } = await import("@/components/ai-toolbar");

	renderToStaticMarkup(
		actualReact.createElement(AIToolbar, createToolbarProps(overrides)),
	);

	return {
		buttons,
		getButtonByLabel: (label: string) => {
			const button = buttons.find((entry) => entry.label.includes(label));
			expect(button).toBeDefined();
			return button!;
		},
		getButtonByAriaLabel: (ariaLabel: string) => {
			const button = buttons.find((entry) => entry.ariaLabel === ariaLabel);
			expect(button).toBeDefined();
			return button!;
		},
	};
};

export const getDocumentListener = (
	type: string,
	predicate: (entry: {
		listener: EventListener;
		options?: AddEventListenerOptions;
	}) => boolean = () => true,
) => {
	const entry = registeredDocumentListeners.get(type)?.find(predicate);
	expect(entry).toBeDefined();
	return entry!.listener;
};

export const getDocumentListenerMocks = () => ({
	addEventListener: documentAddEventListener,
	removeEventListener: documentRemoveEventListener,
	lastEffectCleanup,
});

export const resetRegisteredDocumentListeners = () => {
	registeredDocumentListeners.clear();
};

export const createClipboardData = (files: File[]): DataTransfer =>
	({
		files,
		items: files.map((file) => ({
			kind: "file",
			type: file.type,
			getAsFile: () => file,
		})),
	} as unknown as DataTransfer);

export const createTextareaKeydownEvent = (
	overrides: Partial<React.KeyboardEvent<HTMLTextAreaElement>> = {},
) => ({
	key: "Enter",
	ctrlKey: false,
	metaKey: false,
	shiftKey: false,
	altKey: false,
	preventDefault: mock(),
	...overrides,
}) as unknown as React.KeyboardEvent<HTMLTextAreaElement>;

export const createDocumentKeydownEvent = (
	overrides: Partial<KeyboardEvent> = {},
) => ({
	key: "v",
	ctrlKey: false,
	metaKey: false,
	shiftKey: false,
	altKey: false,
	target: { tagName: "DIV", isContentEditable: false } as unknown as EventTarget,
	...overrides,
}) as unknown as KeyboardEvent;

export const setMockNavigatorClipboardRead = (read: () => Promise<unknown>) => {
	(globalThis as { navigator?: Navigator }).navigator = {
		clipboard: { read },
	} as unknown as Navigator;
};

const escapeForRegex = (value: string) =>
	value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

export const getButtonOpeningTag = (markup: string, label: string) => {
	const buttonMatch = markup.match(
		new RegExp(
			`<button\\b[^>]*>(?:(?!<\\/button>)[\\s\\S])*?${escapeForRegex(label)}(?:(?!<\\/button>)[\\s\\S])*?<\\/button>`,
		),
	);
	const openingTagMatch = buttonMatch?.[0].match(/^<button\b[^>]*>/);

	expect(openingTagMatch).toBeDefined();

	return openingTagMatch![0];
};

const getClassAttributes = (markup: string) =>
	[...markup.matchAll(/class="([^"]+)"/g)].map(([, className]) => className);

export const getMatchingClassTokens = (
	markup: string,
	predicate: (tokens: string[]) => boolean,
) =>
	getClassAttributes(markup)
		.map((className) => className.split(/\s+/).filter(Boolean))
		.filter(predicate);
