import { parseTheme } from "@ax/helpers";
import * as hooks from "@ax/hooks";
import FramePreview from "@ax/modules/FramePreview";
import * as frameUtils from "@ax/modules/FramePreview/utils";
import globalTheme from "@ax/themes/theme.json";

import "@testing-library/jest-dom";
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import { ThemeProvider } from "styled-components";

import { act, cleanup, render, screen } from "../../../../config/jest/test-utils";

// Capture BrowserContent props so individual callback functions can be tested
const capturedBrowserContentProps: { current: Record<string, any> | null } = { current: null };

jest.mock("@ax/hooks", () => ({
	...jest.requireActual("@ax/hooks"),
	useURLSearchParam: jest.fn().mockReturnValue(null),
	useOnMessageReceivedFromOutside: jest.fn(),
	useToast: jest.fn().mockReturnValue({
		isVisible: false,
		toggleToast: jest.fn(),
		setIsVisible: jest.fn(),
		state: "",
	}),
}));

jest.mock("@ax/components", () => {
	const React = require("react");
	return {
		BrowserContent: (props: any) => {
			capturedBrowserContentProps.current = props;
			return React.createElement("div", { "data-testid": "browser-content" });
		},
		Loading: () => React.createElement("div", { "data-testid": "loading-wrapper" }),
		Toast: () => React.createElement("div", { "data-testid": "toast-wrapper" }),
	};
});

jest.mock("@ax/modules/FramePreview/utils", () => ({
	observeAndAddIdsToHeadings: jest.fn().mockReturnValue(jest.fn()),
	observeAndHighlightKeywords: jest.fn().mockReturnValue(jest.fn()),
}));

jest.mock("@ax/forms", () => ({
	findByEditorID: jest.fn().mockReturnValue({
		element: { component: "Page", editorID: 5 },
		parent: { parentEditorID: 0 },
	}),
}));

// Mock action creators as plain functions to avoid thunk side-effects
jest.mock("@ax/containers/PageEditor", () => ({
	pageEditorActions: {
		setSelectedContent: (id: number) => ({ type: "SET_SELECTED_CONTENT", payload: id }),
		setEditorContent: (content: any) => ({ type: "SET_EDITOR_CONTENT", payload: content }),
		deleteModule: (ids: number[]) => ({ type: "DELETE_MODULE", payload: ids }),
		duplicateModule: (ids: number[]) => ({ type: "DUPLICATE_MODULE", payload: ids }),
		copyModule: (ids: number[]) => ({ type: "COPY_MODULE", payload: ids }),
	},
}));

jest.mock("@ax/containers/Forms", () => ({
	formsActions: {
		setSelectedContent: (id: number) => ({ type: "SET_FORM_SELECTED_CONTENT", payload: id }),
		setFormContent: (content: any) => ({ type: "SET_FORM_CONTENT", payload: content }),
	},
}));

const middlewares: any = [thunk];
const mockStore = configureStore(middlewares);

const buildStore = (appOverrides = {}) =>
	mockStore({
		pageEditor: { editorContent: {} },
		forms: { formContent: {} },
		social: {},
		app: {
			globalSettings: { cloudinaryName: null },
			globalLangs: [],
			isLoading: false,
			...appOverrides,
		},
		sites: { currentSiteInfo: null, currentSiteLanguages: [] },
	});

const renderFramePreview = async (store = buildStore()) => {
	await act(async () => {
		render(
			<ThemeProvider theme={parseTheme(globalTheme)}>
				<FramePreview />
			</ThemeProvider>,
			{ store },
		);
	});
};

afterEach(() => {
	cleanup();
	capturedBrowserContentProps.current = null;
	jest.restoreAllMocks();
});

describe("FramePreview rendering", () => {
	it("should render Loading when isLoading is true from Redux state", async () => {
		await renderFramePreview(buildStore({ isLoading: true }));

		expect(screen.getByTestId("loading-wrapper")).toBeTruthy();
	});

	it("should render BrowserContent when not loading", async () => {
		await renderFramePreview();

		expect(screen.getByTestId("browser-content")).toBeTruthy();
	});

	it("should not render HeadingsOverlay when type is not 'headings'", async () => {
		await renderFramePreview();

		expect(screen.queryByTestId("headings-overlay")).toBeNull();
	});

	it("should render HeadingsOverlay when type is 'headings'", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "type") return "headings";
			return null;
		});

		await renderFramePreview();

		expect(screen.getByTestId("headings-overlay")).toBeTruthy();
	});
});

describe("FramePreview effects", () => {
	it("should post iframe-mousemove message to parent on window mousemove", async () => {
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		act(() => {
			window.dispatchEvent(new MouseEvent("mousemove", { clientX: 150 }));
		});

		expect(postMessageSpy).toHaveBeenCalledWith(
			expect.objectContaining({ type: "iframe-mousemove", clientX: 150 }),
			"*",
		);
	});

	it("should set selectedID to '0' in localStorage when disabled param is true", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "disabled") return "true";
			return null;
		});

		await renderFramePreview();

		expect(localStorage.getItem("selectedID")).toBe("0");
	});

	it("should call observeAndAddIdsToHeadings when type is 'headings'", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "type") return "headings";
			return null;
		});

		await renderFramePreview();

		expect(frameUtils.observeAndAddIdsToHeadings).toHaveBeenCalledWith(document.body, null);
	});

	it("should call observeAndHighlightKeywords when type is 'keywords'", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "type") return "keywords";
			return null;
		});

		await renderFramePreview();

		expect(frameUtils.observeAndHighlightKeywords).toHaveBeenCalledWith(document.body, []);
	});
});

describe("FramePreview module actions", () => {
	it("should post module-delete and dispatch deleteModule when deleteModuleAction is called", async () => {
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		capturedBrowserContentProps.current!.moduleActions.deleteModuleAction(1);

		expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-delete", message: 1 }, "*");
	});

	it("should post module-duplicate when duplicateModuleAction is called", async () => {
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		capturedBrowserContentProps.current!.moduleActions.duplicateModuleAction(2);

		expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-duplicate", message: 2 }, "*");
	});

	it("should post module-copy and call toggleToast when copyModuleAction is called", async () => {
		const toggleToastMock = jest.fn();
		(hooks.useToast as jest.Mock).mockReturnValue({
			isVisible: false,
			toggleToast: toggleToastMock,
			setIsVisible: jest.fn(),
			state: "",
		});
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		capturedBrowserContentProps.current!.moduleActions.copyModuleAction(3);

		expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-copy", message: 3 }, "*");
		expect(toggleToastMock).toHaveBeenCalledWith("1 module copied to clipboard");
	});
});

describe("FramePreview editor ID selection", () => {
	it("should post module-click when selectEditorID is called", async () => {
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		const { selectEditorID } = capturedBrowserContentProps.current!;
		const mockEvent = { stopPropagation: jest.fn() };
		selectEditorID({ editorID: 1, component: "Page", type: "content", parentEditorID: 0 }, null, mockEvent);

		expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-click", message: 1 }, "*");
	});

	it("should post module-scroll when selectHoverEditorID is called and parent meets condition", async () => {
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		const { selectHoverEditorID } = capturedBrowserContentProps.current!;
		selectHoverEditorID(5, 0);

		expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-scroll", message: 5 }, "*");
	});

	it("should not post module-click when preview mode is active", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "preview") return "true";
			return null;
		});
		const postMessageSpy = jest.spyOn(window, "postMessage");

		await renderFramePreview();

		const { selectEditorID } = capturedBrowserContentProps.current!;
		const mockEvent = { stopPropagation: jest.fn() };
		selectEditorID({ editorID: 1, component: "Page", type: "content", parentEditorID: 0 }, null, mockEvent);

		expect(postMessageSpy).not.toHaveBeenCalledWith(expect.objectContaining({ type: "module-click" }), "*");
	});
});

describe("FramePreview content modes", () => {
	it("should render Toast when isVisible is true", async () => {
		(hooks.useToast as jest.Mock).mockReturnValueOnce({
			isVisible: true,
			toggleToast: jest.fn(),
			setIsVisible: jest.fn(),
			state: "1 module copied to clipboard",
		});

		await renderFramePreview();

		expect(screen.getByTestId("toast-wrapper")).toBeTruthy();
	});

	it("should use 'preview' renderer when preview param is true", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "preview") return "true";
			return null;
		});

		await renderFramePreview();

		expect(capturedBrowserContentProps.current?.renderer).toBe("preview");
	});

	it("should pass filtered keywords to observeAndHighlightKeywords when keywordFilter param is set", async () => {
		(hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
			if (param === "type") return "keywords";
			if (param === "keywordFilter") return "seo";
			return null;
		});

		await renderFramePreview();

		expect(frameUtils.observeAndHighlightKeywords).toHaveBeenCalledWith(document.body, ["seo"]);
	});

	it("should use currentSiteInfo theme and langs when currentSiteInfo is provided", async () => {
		const store = mockStore({
			pageEditor: { editorContent: {} },
			forms: { formContent: {} },
			social: {},
			app: { globalSettings: { cloudinaryName: null }, globalLangs: [], isLoading: false },
			sites: { currentSiteInfo: { id: 42, theme: "custom-theme" }, currentSiteLanguages: [{ id: 1 }] },
		});

		await renderFramePreview(store);

		expect(capturedBrowserContentProps.current?.theme).toBe("custom-theme");
		expect(capturedBrowserContentProps.current?.siteID).toBe(42);
	});
});
