import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { m } from '$lib/paraglide/messages.js';
import type { NotificationItem } from '$lib/notification/notifications';
import NotificationFixture from './notification.test-fixture.svelte';

vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn() }));
vi.mock('$app/forms', () => ({
	enhance(node: HTMLFormElement, submit?: (opts: { formData: FormData }) => unknown) {
		const handler = (e: Event) => {
			e.preventDefault();
			submit?.({ formData: new FormData(node) } as never);
		};
		node.addEventListener('submit', handler);
		return { destroy: () => node.removeEventListener('submit', handler) };
	}
}));

// NotificationBell.onMount calls store.init(), which opens an EventSource and
// fetches the unread count. Stub both so no real network or 401 → /login
// navigation fires; the real store + provideNotificationStore() run otherwise.
class NoopEventSource {
	static CLOSED = 2;
	readyState = 0;
	onopen: (() => void) | null = null;
	onerror: (() => void) | null = null;
	addEventListener() {}
	close() {}
}

beforeEach(() => {
	vi.stubGlobal('EventSource', NoopEventSource);
	// init()'s fetchUnreadCount() never settles, so the bell's announced count is
	// driven solely by setNotifications() — removes the init-fetch-vs-setNotifications
	// race (the real fetch would resolve to {count:0} and clobber the seeded count).
	vi.stubGlobal(
		'fetch',
		vi.fn(() => new Promise(() => {}))
	);
});

afterEach(() => {
	cleanup();
	vi.clearAllMocks();
	vi.unstubAllGlobals();
});

const tick = () => new Promise((r) => setTimeout(r, 0));

const makeNotification = (overrides: Partial<NotificationItem> = {}): NotificationItem => ({
	id: 'n1',
	type: 'REPLY',
	documentId: 'doc-1',
	referenceId: 'ref-1',
	annotationId: null,
	read: false,
	createdAt: '2026-04-21T10:00:00Z',
	actorName: 'Anna',
	documentTitle: 'Test Doc',
	...overrides
});

type Api = { setNotifications: (items: NotificationItem[]) => void };

function renderBell(): Api {
	let api: Api = { setNotifications: () => {} };
	render(NotificationFixture, { onReady: (a: Api) => (api = a) });
	return api;
}

function bellButton(): HTMLButtonElement {
	return document.querySelector<HTMLButtonElement>('button[aria-haspopup="true"]')!;
}

function unreadBadge(): HTMLElement {
	return bellButton().querySelector<HTMLElement>('[aria-live="polite"]')!;
}

describe('NotificationBell — rendering', () => {
	it('bell button has cursor-pointer class', async () => {
		renderBell();
		await tick();
		expect(bellButton().classList.contains('cursor-pointer')).toBe(true);
	});
});

// AC#5: the bell's announced unread count must hold across the four a11y states.
// The count is announced via the aria-live badge text and the button title /
// aria-label; both must stay consistent as the store's notifications change.
describe('NotificationBell — announced unread count across a11y states', () => {
	it('empty: announces no unread count and hides the live badge', async () => {
		const { setNotifications } = renderBell();
		setNotifications([]);
		await tick();

		const btn = bellButton();
		expect(btn.getAttribute('title')).toBe(m.notification_bell_label());
		expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
		expect(unreadBadge().classList.contains('hidden')).toBe(true);
	});

	it('single: announces one unread notification', async () => {
		const { setNotifications } = renderBell();
		setNotifications([makeNotification({ id: 'n1', read: false })]);
		await tick();

		const btn = bellButton();
		expect(btn.getAttribute('title')).toBe(m.notification_bell_unread_label({ count: 1 }));
		expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
		expect(unreadBadge().classList.contains('hidden')).toBe(false);
		expect(unreadBadge().textContent?.trim()).toBe('1');
	});

	it('many: announces the exact unread count', async () => {
		const { setNotifications } = renderBell();
		setNotifications([
			makeNotification({ id: 'n1', read: false }),
			makeNotification({ id: 'n2', read: false }),
			makeNotification({ id: 'n3', read: false })
		]);
		await tick();

		const btn = bellButton();
		expect(btn.getAttribute('title')).toBe(m.notification_bell_unread_label({ count: 3 }));
		expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
		expect(unreadBadge().textContent?.trim()).toBe('3');
	});

	it('error: a failed unread-count load does not wipe the announced count', async () => {
		vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')));
		const { setNotifications } = renderBell();
		setNotifications([
			makeNotification({ id: 'n1', read: false }),
			makeNotification({ id: 'n2', read: false })
		]);
		await tick();

		// init()'s fetchUnreadCount rejects; its catch must leave the already-set
		// count intact rather than silently zeroing the live region. This is a
		// distinct state from "empty" — the count survived a transient load failure.
		const btn = bellButton();
		expect(btn.getAttribute('title')).toBe(m.notification_bell_unread_label({ count: 2 }));
		expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
		expect(unreadBadge().textContent?.trim()).toBe('2');
	});
});
