import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
import { useNotifications } from "./use-notifications";
import { NotificationProvider } from "../state/notifications";

vi.mock("../api/notifications", () => ({
	getNotifications: vi.fn(),
	getUnreadCount: vi.fn(),
	markNotificationRead: vi.fn(),
	markAllNotificationsRead: vi.fn(),
	dismissNotification: vi.fn(),
}));

import {
	getNotifications,
	getUnreadCount,
	markNotificationRead,
	markAllNotificationsRead,
	dismissNotification,
} from "../api/notifications";
import type { NotificationItem } from "../api/notifications";

const mockedGetNotifications = vi.mocked(getNotifications);
const mockedGetUnreadCount = vi.mocked(getUnreadCount);
const mockedMarkNotificationRead = vi.mocked(markNotificationRead);
const mockedMarkAllNotificationsRead = vi.mocked(markAllNotificationsRead);
const mockedDismissNotification = vi.mocked(dismissNotification);

function wrapper({ children }: { children: React.ReactNode }) {
	return <NotificationProvider>{children}</NotificationProvider>;
}

const makeNotification = (id: string, overrides?: Record<string, unknown>) => ({
	id,
	user_id: "user-1",
	category: "instance",
	severity: "info" as const,
	title: "Test",
	message: null,
	source_type: null,
	source_id: null,
	metadata: {},
	read_at: null,
	dismissed_at: null,
	created_at: "2026-05-29T10:00:00Z",
	...overrides,
});

describe("useNotifications", () => {
	beforeEach(() => {
		vi.useFakeTimers({ shouldAdvanceTime: true });
		mockedGetNotifications.mockResolvedValue({
			items: [],
			total: 0,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(0);
		mockedMarkNotificationRead.mockResolvedValue(
			makeNotification("1", { read_at: "2026-05-29T10:01:00Z" }),
		);
		mockedMarkAllNotificationsRead.mockResolvedValue(1);
		mockedDismissNotification.mockResolvedValue(undefined);
	});

	afterEach(() => {
		vi.useRealTimers();
		vi.clearAllMocks();
	});

	it("returns notifications and unreadCount from provider", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1")],
			total: 1,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(3);

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});

		await waitFor(() => {
			expect(result.current.notifications).toHaveLength(1);
			expect(result.current.unreadCount).toBe(3);
		});
	});

	it("optimistically updates on markRead", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1"), makeNotification("2")],
			total: 2,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(2);

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});

		await waitFor(() => expect(result.current.unreadCount).toBe(2));

		let resolveApi:
			| ((value: NotificationItem | PromiseLike<NotificationItem>) => void)
			| undefined;
		mockedMarkNotificationRead.mockReturnValue(
			new Promise((resolve) => {
				resolveApi = resolve;
			}),
		);

		act(() => {
			void result.current.markRead("1");
		});

		await waitFor(() => {
			const n = result.current.notifications.find((x) => x.id === "1");
			expect(n?.read_at).not.toBeNull();
		});
		expect(result.current.unreadCount).toBe(1);

		act(() => {
			resolveApi?.(makeNotification("1", { read_at: "2026-05-29T10:01:00Z" }));
		});
	});

	it("reverts optimistic update on markRead failure", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1")],
			total: 1,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(1);

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});
		await waitFor(() => expect(result.current.unreadCount).toBe(1));

		mockedMarkNotificationRead.mockRejectedValue(new Error("Network error"));

		await act(async () => {
			await result.current.markRead("1");
		});

		const n = result.current.notifications.find((x) => x.id === "1");
		expect(n?.read_at).toBeNull();
		expect(result.current.unreadCount).toBe(1);
		expect(result.current.error).toBeInstanceOf(Error);
	});

	it("optimistically updates on dismiss", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1"), makeNotification("2")],
			total: 2,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(2);

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});
		await waitFor(() => expect(result.current.notifications).toHaveLength(2));

		mockedDismissNotification.mockReturnValue(new Promise(() => {}));

		act(() => {
			void result.current.dismiss("1");
		});

		await waitFor(() => {
			expect(result.current.notifications).toHaveLength(1);
		});
		expect(result.current.unreadCount).toBe(1);
	});

	it("reverts optimistic update on dismiss failure", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1")],
			total: 1,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(1);

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});
		await waitFor(() => expect(result.current.notifications).toHaveLength(1));

		mockedDismissNotification.mockRejectedValue(new Error("Network error"));

		await act(async () => {
			await result.current.dismiss("1");
		});

		expect(result.current.notifications).toHaveLength(1);
		expect(result.current.unreadCount).toBe(1);
		expect(result.current.error).toBeInstanceOf(Error);
	});

	it("calls refreshList when invoked", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1")],
			total: 1,
			limit: 20,
			offset: 0,
		});

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});

		mockedGetNotifications.mockResolvedValue({
			items: [makeNotification("1"), makeNotification("2")],
			total: 2,
			limit: 20,
			offset: 0,
		});

		await act(async () => {
			await result.current.refreshList();
		});

		expect(mockedGetNotifications).toHaveBeenCalledTimes(2);
		await waitFor(() => expect(result.current.notifications).toHaveLength(2));
	});

	it("stops polling on 401", async () => {
		mockedGetUnreadCount.mockRejectedValue({ response: { status: 401 } });

		renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});

		const callCountAfterFirst = mockedGetUnreadCount.mock.calls.length;

		await act(async () => {
			vi.advanceTimersByTime(60000);
		});

		expect(mockedGetUnreadCount.mock.calls.length).toBe(callCountAfterFirst);
	});

	it("pauses polling when document hidden", async () => {
		renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});

		const callCountBefore = mockedGetUnreadCount.mock.calls.length;

		act(() => {
			Object.defineProperty(document, "hidden", {
				value: true,
				writable: true,
				configurable: true,
			});
			document.dispatchEvent(new Event("visibilitychange"));
		});

		await act(async () => {
			vi.advanceTimersByTime(60000);
		});

		expect(mockedGetUnreadCount.mock.calls.length).toBe(callCountBefore);

		act(() => {
			Object.defineProperty(document, "hidden", {
				value: false,
				writable: true,
				configurable: true,
			});
			document.dispatchEvent(new Event("visibilitychange"));
		});

		await act(async () => {
			vi.advanceTimersByTime(100);
		});

		expect(mockedGetUnreadCount.mock.calls.length).toBeGreaterThan(
			callCountBefore,
		);
	});

	it("multiple markRead calls decrement correctly", async () => {
		mockedGetNotifications.mockResolvedValue({
			items: [
				makeNotification("1"),
				makeNotification("2"),
				makeNotification("3"),
			],
			total: 3,
			limit: 20,
			offset: 0,
		});
		mockedGetUnreadCount.mockResolvedValue(3);

		const { result } = renderHook(() => useNotifications(), { wrapper });

		await act(async () => {
			vi.advanceTimersByTime(100);
		});
		await waitFor(() => expect(result.current.unreadCount).toBe(3));

		mockedMarkNotificationRead.mockResolvedValue(
			makeNotification("1", { read_at: "2026-05-29T10:01:00Z" }),
		);

		await act(async () => {
			await result.current.markRead("1");
			await result.current.markRead("2");
			await result.current.markRead("3");
		});

		expect(result.current.unreadCount).toBe(0);
	});
});
