import { describe, test, expect, beforeEach, vi, beforeAll } from 'vitest';
// @ts-expect-error - web-push doesn't have TypeScript types, but we mock it anyway
import webpush from 'web-push';

// Mock web-push module BEFORE importing the service
vi.mock('web-push', () => ({
	default: {
		setVapidDetails: vi.fn(),
		sendNotification: vi.fn()
	}
}));

// Import service AFTER mocking
import { pushNotificationService } from '$lib/server/notifications/PushNotificationService';

describe('PushNotificationService web-push integration', () => {
	beforeEach(() => {
		vi.clearAllMocks();
		// Clear all subscriptions before each test
		pushNotificationService.clearAllSubscriptions();
	});

	test('should have VAPID public key configured', () => {
		// Verify the service has a public VAPID key available
		const publicKey = pushNotificationService.getPublicVapidKey();
		expect(publicKey).toBeTruthy();
		expect(typeof publicKey).toBe('string');
		expect(publicKey!.length).toBeGreaterThan(0);
	});

	test('should send notification with web-push', async () => {
		const mockSubscription = {
			endpoint: 'https://push.example.com/test',
			keys: { p256dh: 'test-key', auth: 'test-auth' }
		};

		vi.mocked(webpush.sendNotification).mockResolvedValue({} as any);

		await pushNotificationService.subscribe('client-1', mockSubscription);
		await pushNotificationService.sendNotification({
			type: 'success',
			itemId: 'test-123',
			body: 'Test notification'
		});

		expect(webpush.sendNotification).toHaveBeenCalledWith(
			expect.objectContaining({
				endpoint: mockSubscription.endpoint,
				keys: mockSubscription.keys
			}),
			expect.any(String),
			expect.objectContaining({
				TTL: 60 * 60 * 24
			})
		);
	});

	test('should handle subscription expiration (410)', async () => {
		const mockError: any = new Error('Gone');
		mockError.statusCode = 410;

		vi.mocked(webpush.sendNotification).mockRejectedValue(mockError);

		const mockSubscription = {
			endpoint: 'https://push.example.com/expired',
			keys: { p256dh: 'test', auth: 'test' }
		};

		await pushNotificationService.subscribe('client-1', mockSubscription);

		// Verify subscription exists before sending
		expect(pushNotificationService.getSubscriptionCount()).toBe(1);

		// sendNotification catches errors internally and removes invalid subscriptions
		// It doesn't throw, so we just await it
		await pushNotificationService.sendNotification({
			type: 'error',
			itemId: 'test',
			body: 'Test'
		});

		// Verify the subscription was removed due to 410 error
		expect(pushNotificationService.getSubscriptionCount()).toBe(0);
	});

	test('should send notification with TTL of 24 hours', async () => {
		const mockSubscription = {
			endpoint: 'https://push.example.com/test-ttl',
			keys: { p256dh: 'test-key', auth: 'test-auth' }
		};

		vi.mocked(webpush.sendNotification).mockResolvedValue({} as any);

		await pushNotificationService.subscribe('client-2', mockSubscription);
		await pushNotificationService.sendNotification({
			type: 'progress',
			itemId: 'test-456',
			body: 'Progress update'
		});

		expect(webpush.sendNotification).toHaveBeenCalledWith(expect.any(Object), expect.any(String), {
			TTL: 60 * 60 * 24
		});
	});

	test('should serialize notification data as JSON', async () => {
		const mockSubscription = {
			endpoint: 'https://push.example.com/test-json',
			keys: { p256dh: 'test-key', auth: 'test-auth' }
		};

		vi.mocked(webpush.sendNotification).mockResolvedValue({} as any);

		const testPayload = {
			type: 'success' as const,
			itemId: 'test-789',
			body: 'JSON test',
			recipeName: 'Test Recipe'
		};

		await pushNotificationService.subscribe('client-3', mockSubscription);
		await pushNotificationService.sendNotification(testPayload);

		const sendCallArgs = vi.mocked(webpush.sendNotification).mock.calls[0];
		const sentPayload = sendCallArgs[1];

		// Verify the payload is stringified JSON
		expect(typeof sentPayload).toBe('string');
		const parsedPayload = JSON.parse(sentPayload);
		expect(parsedPayload).toMatchObject({
			type: 'success',
			itemId: 'test-789',
			body: 'JSON test',
			recipeName: 'Test Recipe'
		});
	});

	test('should handle multiple subscriptions', async () => {
		const mockSubscription1 = {
			endpoint: 'https://push.example.com/client1',
			keys: { p256dh: 'key1', auth: 'auth1' }
		};
		const mockSubscription2 = {
			endpoint: 'https://push.example.com/client2',
			keys: { p256dh: 'key2', auth: 'auth2' }
		};

		vi.mocked(webpush.sendNotification).mockResolvedValue({} as any);

		await pushNotificationService.subscribe('client-1', mockSubscription1);
		await pushNotificationService.subscribe('client-2', mockSubscription2);

		await pushNotificationService.sendNotification({
			type: 'success',
			itemId: 'test-multi',
			body: 'Multi-subscriber test'
		});

		// Should have sent to both subscribers
		expect(webpush.sendNotification).toHaveBeenCalledTimes(2);
	});

	test('should log endpoint prefix only (privacy)', async () => {
		const consoleSpy = vi.spyOn(console, 'log');

		const longEndpoint =
			'https://push.example.com/very-long-endpoint-with-secret-tokens-that-should-not-be-fully-logged-12345678901234567890';
		const mockSubscription = {
			endpoint: longEndpoint,
			keys: { p256dh: 'test-key', auth: 'test-auth' }
		};

		vi.mocked(webpush.sendNotification).mockResolvedValue({} as any);

		await pushNotificationService.subscribe('client-privacy', mockSubscription);
		await pushNotificationService.sendNotification({
			type: 'success',
			itemId: 'test-privacy',
			body: 'Privacy test'
		});

		// Find the log call with endpoint
		const endpointLogCall = consoleSpy.mock.calls.find(
			(call) => typeof call[0] === 'string' && call[0].includes('Sent notification to')
		);

		expect(endpointLogCall).toBeTruthy();
		// Should log only first 50 chars + ellipsis, not the full endpoint
		expect(endpointLogCall![0]).toContain(longEndpoint.substring(0, 50));
		expect(endpointLogCall![0]).not.toContain('secret-tokens');
	});
});
