import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, act } from "@testing-library/react";
import { EventToastBridge } from "./event-toast-bridge";
import { useEventContext } from "../state/events";
import { getUserConfig } from "../api/settings";
import { handleEventToast } from "./toast-rules";
import type { InstanceEventPayload } from "../types/events";

vi.mock("../state/events", () => ({
	useEventContext: vi.fn(),
}));

vi.mock("../api/settings", () => ({
	getUserConfig: vi.fn(),
}));

vi.mock("./toast-rules", async (importOriginal) => {
	const actual = await importOriginal<typeof import("./toast-rules")>();
	return {
		...actual,
		handleEventToast: vi.fn(),
		clearToastDedup: vi.fn(),
	};
});

const mockedUseEventContext = vi.mocked(useEventContext);
const mockedGetUserConfig = vi.mocked(getUserConfig);
const mockedHandleEventToast = vi.mocked(handleEventToast);

function makeEvent(
	eventType: string,
	overrides?: Partial<InstanceEventPayload>,
): InstanceEventPayload {
	return {
		event: eventType,
		instance_id: "i-1",
		status: undefined,
		message: undefined,
		metadata: {},
		timestamp: "2026-05-29T10:00:00Z",
		correlation_id: "c1",
		...overrides,
	};
}

async function flushPromises() {
	await act(async () => {
		await Promise.resolve();
	});
}

describe("EventToastBridge preference checks", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		mockedUseEventContext.mockReturnValue({
			events: [],
			connected: false,
			reconnectCount: 0,
		});
		mockedGetUserConfig.mockResolvedValue({
			theme: "system",
			default_editor: null,
			git_user_name: null,
			git_user_email: null,
			last_session_id: null,
			notification_toast_level: "all",
			notification_mute_categories: [],
		} as unknown as Awaited<ReturnType<typeof getUserConfig>>);
	});

	afterEach(() => {
		vi.restoreAllMocks();
	});

	it("shows toast when level is all and category not muted", async () => {
		const event = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).toHaveBeenCalledWith(event);
	});

	it("suppresses toast when level is none", async () => {
		mockedGetUserConfig.mockResolvedValue({
			notification_toast_level: "none",
			notification_mute_categories: [],
		} as unknown as Awaited<ReturnType<typeof getUserConfig>>);
		const event = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).not.toHaveBeenCalled();
	});

	it("suppresses info toast when level is errors", async () => {
		mockedGetUserConfig.mockResolvedValue({
			notification_toast_level: "errors",
			notification_mute_categories: [],
		} as unknown as Awaited<ReturnType<typeof getUserConfig>>);
		const event = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).not.toHaveBeenCalled();
	});

	it("shows error toast when level is errors", async () => {
		mockedGetUserConfig.mockResolvedValue({
			notification_toast_level: "errors",
			notification_mute_categories: [],
		} as unknown as Awaited<ReturnType<typeof getUserConfig>>);
		const event = makeEvent("instance.error");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).toHaveBeenCalledWith(event);
	});

	it("suppresses toast when category is muted", async () => {
		mockedGetUserConfig.mockResolvedValue({
			notification_toast_level: "all",
			notification_mute_categories: ["instance"],
		} as unknown as Awaited<ReturnType<typeof getUserConfig>>);
		const event = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).not.toHaveBeenCalled();
	});

	it("applies preference change immediately via custom event", async () => {
		const event1 = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event1],
			connected: false,
			reconnectCount: 0,
		});
		const { rerender } = render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).toHaveBeenCalledTimes(1);

		act(() => {
			window.dispatchEvent(
				new CustomEvent("userconfig:updated", {
					detail: { notification_toast_level: "none" },
				}),
			);
		});

		const event2 = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event1, event2],
			connected: false,
			reconnectCount: 0,
		});
		rerender(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).toHaveBeenCalledTimes(1);
	});

	it("muted category overrides all level", async () => {
		mockedGetUserConfig.mockResolvedValue({
			notification_toast_level: "all",
			notification_mute_categories: ["instance"],
		} as unknown as Awaited<ReturnType<typeof getUserConfig>>);
		const event = makeEvent("instance.error");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).not.toHaveBeenCalled();
	});

	it("deduplication still works with preferences", async () => {
		const event = makeEvent("instance.started");
		mockedUseEventContext.mockReturnValue({
			events: [event, event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).toHaveBeenCalledTimes(1);
	});

	it("unmapped event defaults to system/info and shows when level is all", async () => {
		const event = makeEvent("system.announcement");
		mockedUseEventContext.mockReturnValue({
			events: [event],
			connected: false,
			reconnectCount: 0,
		});
		render(<EventToastBridge />);
		await flushPromises();
		expect(mockedHandleEventToast).toHaveBeenCalledWith(event);
	});
});
