import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

/**
 * Unit tests for the Web Vitals monitoring module (webVitals.ts).
 *
 * Tests metric collection initialization, rating calculations against
 * Core Web Vitals 2024 thresholds, and the default reporter behavior
 * (console logging in debug mode, sendBeacon/fetch in production).
 */

// Capture the callbacks registered by initWebVitals
const mockOnLCP = vi.fn();
const mockOnCLS = vi.fn();
const mockOnINP = vi.fn();
const mockOnFCP = vi.fn();
const mockOnTTFB = vi.fn();

vi.mock('web-vitals', () => ({
	onLCP: mockOnLCP,
	onCLS: mockOnCLS,
	onINP: mockOnINP,
	onFCP: mockOnFCP,
	onTTFB: mockOnTTFB
}));

describe('webVitals monitoring', () => {
	let webVitalsModule: typeof import('$lib/monitoring/webVitals');

	beforeEach(async () => {
		vi.clearAllMocks();
		vi.spyOn(console, 'log').mockImplementation(() => {});

		vi.resetModules();
		webVitalsModule = await import('$lib/monitoring/webVitals');
	});

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

	// ==========================================================================
	// initWebVitals
	// ==========================================================================
	describe('initWebVitals', () => {
		it('should register callbacks for all five web vitals', () => {
			const reporter = vi.fn();

			webVitalsModule.initWebVitals(reporter);

			expect(mockOnLCP).toHaveBeenCalledOnce();
			expect(mockOnCLS).toHaveBeenCalledOnce();
			expect(mockOnINP).toHaveBeenCalledOnce();
			expect(mockOnFCP).toHaveBeenCalledOnce();
			expect(mockOnTTFB).toHaveBeenCalledOnce();
		});

		it('should pass metrics to the reporter when LCP fires', () => {
			const reporter = vi.fn();
			webVitalsModule.initWebVitals(reporter);

			// Simulate LCP metric firing
			const lcpCallback = mockOnLCP.mock.calls[0]![0];
			lcpCallback({ name: 'LCP', value: 2000, delta: 2000, id: 'v1-lcp' });

			expect(reporter).toHaveBeenCalledOnce();
			expect(reporter).toHaveBeenCalledWith(
				expect.objectContaining({
					name: 'LCP',
					value: 2000,
					rating: 'good',
					delta: 2000,
					id: 'v1-lcp'
				})
			);
		});

		it('should pass metrics to the reporter when CLS fires', () => {
			const reporter = vi.fn();
			webVitalsModule.initWebVitals(reporter);

			const clsCallback = mockOnCLS.mock.calls[0]![0];
			clsCallback({ name: 'CLS', value: 0.05, delta: 0.05, id: 'v1-cls' });

			expect(reporter).toHaveBeenCalledWith(
				expect.objectContaining({
					name: 'CLS',
					value: 0.05,
					rating: 'good',
					delta: 0.05,
					id: 'v1-cls'
				})
			);
		});

		it('should pass metrics to the reporter when INP fires', () => {
			const reporter = vi.fn();
			webVitalsModule.initWebVitals(reporter);

			const inpCallback = mockOnINP.mock.calls[0]![0];
			inpCallback({ name: 'INP', value: 150, delta: 150, id: 'v1-inp' });

			expect(reporter).toHaveBeenCalledWith(
				expect.objectContaining({
					name: 'INP',
					value: 150,
					rating: 'good'
				})
			);
		});

		it('should pass metrics to the reporter when FCP fires', () => {
			const reporter = vi.fn();
			webVitalsModule.initWebVitals(reporter);

			const fcpCallback = mockOnFCP.mock.calls[0]![0];
			fcpCallback({ name: 'FCP', value: 1500, delta: 1500, id: 'v1-fcp' });

			expect(reporter).toHaveBeenCalledWith(
				expect.objectContaining({
					name: 'FCP',
					value: 1500,
					rating: 'good'
				})
			);
		});

		it('should pass metrics to the reporter when TTFB fires', () => {
			const reporter = vi.fn();
			webVitalsModule.initWebVitals(reporter);

			const ttfbCallback = mockOnTTFB.mock.calls[0]![0];
			ttfbCallback({ name: 'TTFB', value: 600, delta: 600, id: 'v1-ttfb' });

			expect(reporter).toHaveBeenCalledWith(
				expect.objectContaining({
					name: 'TTFB',
					value: 600,
					rating: 'good'
				})
			);
		});
	});

	// ==========================================================================
	// Rating calculations (via initWebVitals callback pipeline)
	// ==========================================================================
	describe('rating calculations', () => {
		// LCP thresholds: good <= 2500, needs-improvement <= 4000, poor > 4000
		describe('LCP ratings', () => {
			it('should rate LCP as good when value <= 2500', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const lcpCallback = mockOnLCP.mock.calls[0]![0];
				lcpCallback({ name: 'LCP', value: 2500, delta: 2500, id: 'lcp-1' });

				expect(reporter.mock.calls[0]![0].rating).toBe('good');
			});

			it('should rate LCP as needs-improvement when 2500 < value <= 4000', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const lcpCallback = mockOnLCP.mock.calls[0]![0];
				lcpCallback({ name: 'LCP', value: 3000, delta: 3000, id: 'lcp-2' });

				expect(reporter.mock.calls[0]![0].rating).toBe('needs-improvement');
			});

			it('should rate LCP as poor when value > 4000', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const lcpCallback = mockOnLCP.mock.calls[0]![0];
				lcpCallback({ name: 'LCP', value: 5000, delta: 5000, id: 'lcp-3' });

				expect(reporter.mock.calls[0]![0].rating).toBe('poor');
			});
		});

		// FCP thresholds: good <= 1800, needs-improvement <= 3000, poor > 3000
		describe('FCP ratings', () => {
			it('should rate FCP as good when value <= 1800', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const fcpCallback = mockOnFCP.mock.calls[0]![0];
				fcpCallback({ name: 'FCP', value: 1800, delta: 1800, id: 'fcp-1' });

				expect(reporter.mock.calls[0]![0].rating).toBe('good');
			});

			it('should rate FCP as needs-improvement when 1800 < value <= 3000', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const fcpCallback = mockOnFCP.mock.calls[0]![0];
				fcpCallback({ name: 'FCP', value: 2500, delta: 2500, id: 'fcp-2' });

				expect(reporter.mock.calls[0]![0].rating).toBe('needs-improvement');
			});

			it('should rate FCP as poor when value > 3000', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const fcpCallback = mockOnFCP.mock.calls[0]![0];
				fcpCallback({ name: 'FCP', value: 4000, delta: 4000, id: 'fcp-3' });

				expect(reporter.mock.calls[0]![0].rating).toBe('poor');
			});
		});

		// INP thresholds: good <= 200, needs-improvement <= 500, poor > 500
		describe('INP ratings', () => {
			it('should rate INP as good when value <= 200', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const inpCallback = mockOnINP.mock.calls[0]![0];
				inpCallback({ name: 'INP', value: 200, delta: 200, id: 'inp-1' });

				expect(reporter.mock.calls[0]![0].rating).toBe('good');
			});

			it('should rate INP as needs-improvement when 200 < value <= 500', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const inpCallback = mockOnINP.mock.calls[0]![0];
				inpCallback({ name: 'INP', value: 350, delta: 350, id: 'inp-2' });

				expect(reporter.mock.calls[0]![0].rating).toBe('needs-improvement');
			});

			it('should rate INP as poor when value > 500', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const inpCallback = mockOnINP.mock.calls[0]![0];
				inpCallback({ name: 'INP', value: 600, delta: 600, id: 'inp-3' });

				expect(reporter.mock.calls[0]![0].rating).toBe('poor');
			});
		});

		// CLS thresholds: good <= 0.1, needs-improvement <= 0.25, poor > 0.25
		describe('CLS ratings', () => {
			it('should rate CLS as good when value <= 0.1', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const clsCallback = mockOnCLS.mock.calls[0]![0];
				clsCallback({ name: 'CLS', value: 0.1, delta: 0.1, id: 'cls-1' });

				expect(reporter.mock.calls[0]![0].rating).toBe('good');
			});

			it('should rate CLS as needs-improvement when 0.1 < value <= 0.25', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const clsCallback = mockOnCLS.mock.calls[0]![0];
				clsCallback({ name: 'CLS', value: 0.15, delta: 0.15, id: 'cls-2' });

				expect(reporter.mock.calls[0]![0].rating).toBe('needs-improvement');
			});

			it('should rate CLS as poor when value > 0.25', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const clsCallback = mockOnCLS.mock.calls[0]![0];
				clsCallback({ name: 'CLS', value: 0.5, delta: 0.5, id: 'cls-3' });

				expect(reporter.mock.calls[0]![0].rating).toBe('poor');
			});
		});

		// TTFB thresholds: good <= 800, needs-improvement <= 1800, poor > 1800
		describe('TTFB ratings', () => {
			it('should rate TTFB as good when value <= 800', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const ttfbCallback = mockOnTTFB.mock.calls[0]![0];
				ttfbCallback({ name: 'TTFB', value: 800, delta: 800, id: 'ttfb-1' });

				expect(reporter.mock.calls[0]![0].rating).toBe('good');
			});

			it('should rate TTFB as needs-improvement when 800 < value <= 1800', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const ttfbCallback = mockOnTTFB.mock.calls[0]![0];
				ttfbCallback({ name: 'TTFB', value: 1200, delta: 1200, id: 'ttfb-2' });

				expect(reporter.mock.calls[0]![0].rating).toBe('needs-improvement');
			});

			it('should rate TTFB as poor when value > 1800', () => {
				const reporter = vi.fn();
				webVitalsModule.initWebVitals(reporter);

				const ttfbCallback = mockOnTTFB.mock.calls[0]![0];
				ttfbCallback({ name: 'TTFB', value: 2500, delta: 2500, id: 'ttfb-3' });

				expect(reporter.mock.calls[0]![0].rating).toBe('poor');
			});
		});
	});

	// ==========================================================================
	// createDefaultReporter
	// ==========================================================================
	describe('createDefaultReporter', () => {
		it('should return a function', () => {
			const reporter = webVitalsModule.createDefaultReporter({});

			expect(typeof reporter).toBe('function');
		});

		it('should log to console when debug is true', () => {
			const reporter = webVitalsModule.createDefaultReporter({ debug: true });

			reporter({
				name: 'LCP',
				value: 2100.5,
				rating: 'good',
				delta: 2100.5,
				id: 'v1-lcp'
			});

			expect(console.log).toHaveBeenCalledWith(
				'[WebVitals] LCP: 2100.50 (good)'
			);
		});

		it('should not log to console when debug is false', () => {
			const reporter = webVitalsModule.createDefaultReporter({ debug: false });

			reporter({
				name: 'LCP',
				value: 2100,
				rating: 'good',
				delta: 2100,
				id: 'v1-lcp'
			});

			expect(console.log).not.toHaveBeenCalled();
		});

		it('should not log to console when debug is not set', () => {
			const reporter = webVitalsModule.createDefaultReporter({});

			reporter({
				name: 'FCP',
				value: 1500,
				rating: 'good',
				delta: 1500,
				id: 'v1-fcp'
			});

			expect(console.log).not.toHaveBeenCalled();
		});

		it('should use sendBeacon when endpoint is set and sendBeacon is available', () => {
			const mockSendBeacon = vi.fn().mockReturnValue(true);
			vi.stubGlobal('navigator', { sendBeacon: mockSendBeacon });

			const reporter = webVitalsModule.createDefaultReporter({
				endpoint: 'https://analytics.classeo.fr/vitals'
			});

			const metric = {
				name: 'CLS' as const,
				value: 0.05,
				rating: 'good' as const,
				delta: 0.05,
				id: 'v1-cls'
			};

			reporter(metric);

			expect(mockSendBeacon).toHaveBeenCalledWith(
				'https://analytics.classeo.fr/vitals',
				expect.any(String)
			);

			// Verify the payload structure
			const payload = JSON.parse(mockSendBeacon.mock.calls[0]![1]);
			expect(payload).toEqual(
				expect.objectContaining({
					metric: 'CLS',
					value: 0.05,
					rating: 'good',
					delta: 0.05,
					id: 'v1-cls'
				})
			);
			expect(payload.timestamp).toEqual(expect.any(Number));
			expect(payload.url).toEqual(expect.any(String));
		});

		it('should fall back to fetch when sendBeacon is not available', () => {
			vi.stubGlobal('navigator', { sendBeacon: undefined });
			const mockFetch = vi.fn().mockResolvedValue({});
			vi.stubGlobal('fetch', mockFetch);

			const reporter = webVitalsModule.createDefaultReporter({
				endpoint: 'https://analytics.classeo.fr/vitals'
			});

			reporter({
				name: 'LCP',
				value: 2000,
				rating: 'good',
				delta: 2000,
				id: 'v1-lcp'
			});

			expect(mockFetch).toHaveBeenCalledWith(
				'https://analytics.classeo.fr/vitals',
				expect.objectContaining({
					method: 'POST',
					headers: { 'Content-Type': 'application/json' },
					keepalive: true
				})
			);
		});

		it('should not send data when no endpoint is configured', () => {
			const mockSendBeacon = vi.fn();
			vi.stubGlobal('navigator', { sendBeacon: mockSendBeacon });
			const mockFetch = vi.fn();
			vi.stubGlobal('fetch', mockFetch);

			const reporter = webVitalsModule.createDefaultReporter({});

			reporter({
				name: 'INP',
				value: 100,
				rating: 'good',
				delta: 100,
				id: 'v1-inp'
			});

			expect(mockSendBeacon).not.toHaveBeenCalled();
			expect(mockFetch).not.toHaveBeenCalled();
		});

		it('should support both debug and endpoint at the same time', () => {
			const mockSendBeacon = vi.fn().mockReturnValue(true);
			vi.stubGlobal('navigator', { sendBeacon: mockSendBeacon });

			const reporter = webVitalsModule.createDefaultReporter({
				debug: true,
				endpoint: 'https://analytics.classeo.fr/vitals'
			});

			reporter({
				name: 'TTFB',
				value: 500.123,
				rating: 'good',
				delta: 500.123,
				id: 'v1-ttfb'
			});

			expect(console.log).toHaveBeenCalledWith(
				'[WebVitals] TTFB: 500.12 (good)'
			);
			expect(mockSendBeacon).toHaveBeenCalled();
		});
	});
});
