/**
 * Internal dependencies
 */
import {
	detectClientSideMediaSupport,
	isClientSideMediaSupported,
	isHeicCanvasSupported,
	clearFeatureDetectionCache,
	exceedsClientProcessingMemory,
} from '../feature-detection';

describe( 'feature-detection', () => {
	const originalWebAssembly = global.WebAssembly;
	const originalSharedArrayBuffer = global.SharedArrayBuffer;
	const originalWorker = global.Worker;
	const originalCreateObjectURL = global.URL.createObjectURL;
	const originalRevokeObjectURL = global.URL.revokeObjectURL;

	// Store original property descriptors for navigator properties.
	const originalDeviceMemoryDescriptor = Object.getOwnPropertyDescriptor(
		navigator,
		'deviceMemory'
	);
	const originalConnectionDescriptor = Object.getOwnPropertyDescriptor(
		navigator,
		'connection'
	);
	const originalHardwareConcurrencyDescriptor =
		Object.getOwnPropertyDescriptor( navigator, 'hardwareConcurrency' );

	beforeEach( () => {
		// Clear the cache before each test.
		clearFeatureDetectionCache();

		// Restore WebAssembly and SharedArrayBuffer before each test.
		global.WebAssembly = originalWebAssembly;
		global.SharedArrayBuffer = originalSharedArrayBuffer;

		// By default, provide a mock Worker that does not throw (CSP allows blob workers).
		global.Worker = class MockWorker {
			terminate() {}
		} as unknown as typeof Worker;

		// jsdom does not implement URL.createObjectURL/revokeObjectURL.
		global.URL.createObjectURL = jest.fn(
			() => 'blob:http://localhost/test'
		);
		global.URL.revokeObjectURL = jest.fn();

		// Remove navigator.deviceMemory and navigator.connection by default
		// so they don't interfere with unrelated tests.
		if ( 'deviceMemory' in navigator ) {
			// @ts-ignore
			delete navigator.deviceMemory;
		}
		if ( 'connection' in navigator ) {
			// @ts-ignore
			delete navigator.connection;
		}
		if ( 'hardwareConcurrency' in navigator ) {
			// @ts-ignore
			delete navigator.hardwareConcurrency;
		}
	} );

	afterEach( () => {
		// Restore original values.
		global.WebAssembly = originalWebAssembly;
		global.SharedArrayBuffer = originalSharedArrayBuffer;
		global.Worker = originalWorker;
		global.URL.createObjectURL = originalCreateObjectURL;
		global.URL.revokeObjectURL = originalRevokeObjectURL;

		// Restore navigator.deviceMemory.
		if ( originalDeviceMemoryDescriptor ) {
			Object.defineProperty(
				navigator,
				'deviceMemory',
				originalDeviceMemoryDescriptor
			);
		} else if ( 'deviceMemory' in navigator ) {
			// @ts-ignore
			delete navigator.deviceMemory;
		}

		// Restore navigator.connection.
		if ( originalConnectionDescriptor ) {
			Object.defineProperty(
				navigator,
				'connection',
				originalConnectionDescriptor
			);
		} else if ( 'connection' in navigator ) {
			// @ts-ignore
			delete navigator.connection;
		}

		// Restore navigator.hardwareConcurrency.
		if ( originalHardwareConcurrencyDescriptor ) {
			Object.defineProperty(
				navigator,
				'hardwareConcurrency',
				originalHardwareConcurrencyDescriptor
			);
		} else if ( 'hardwareConcurrency' in navigator ) {
			// @ts-ignore
			delete navigator.hardwareConcurrency;
		}
	} );

	describe( 'detectClientSideMediaSupport', () => {
		it( 'returns supported when all features are available', () => {
			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( true );
			expect( result.reason ).toBeUndefined();
		} );

		it( 'returns not supported when WebAssembly is unavailable', () => {
			// @ts-ignore - Intentionally setting WebAssembly to undefined for testing.
			global.WebAssembly = undefined;

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toBe(
				'WebAssembly is not supported in this browser.'
			);
		} );

		it( 'returns not supported when SharedArrayBuffer is unavailable', () => {
			// @ts-ignore - Intentionally setting SharedArrayBuffer to undefined for testing.
			global.SharedArrayBuffer = undefined;

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toContain( 'SharedArrayBuffer' );
		} );

		it( 'returns not supported when Worker is unavailable', () => {
			// @ts-ignore - Intentionally setting Worker to undefined for testing.
			global.Worker = undefined;

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toBe(
				'Web Workers are not supported in this browser.'
			);
		} );

		it( 'returns not supported when device memory is 2 GB or less', () => {
			Object.defineProperty( navigator, 'deviceMemory', {
				value: 2,
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toContain( 'insufficient memory' );
		} );

		it( 'returns supported when device memory is greater than 2 GB', () => {
			Object.defineProperty( navigator, 'deviceMemory', {
				value: 4,
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( true );
		} );

		it( 'returns not supported when hardware concurrency is less than 2', () => {
			Object.defineProperty( navigator, 'hardwareConcurrency', {
				value: 1,
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toContain( 'insufficient CPU cores' );
		} );

		it( 'returns supported when hardware concurrency is 2 or more', () => {
			Object.defineProperty( navigator, 'hardwareConcurrency', {
				value: 2,
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( true );
		} );

		it( 'returns not supported when data saver is enabled', () => {
			Object.defineProperty( navigator, 'connection', {
				value: { saveData: true, effectiveType: '4g' },
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toBe( 'Data saver mode is enabled.' );
		} );

		it( 'returns not supported when connection is 2g', () => {
			Object.defineProperty( navigator, 'connection', {
				value: { saveData: false, effectiveType: '2g' },
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toContain( 'too slow' );
		} );

		it( 'returns supported when connection is 3g', () => {
			Object.defineProperty( navigator, 'connection', {
				value: { saveData: false, effectiveType: '3g' },
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( true );
		} );

		it( 'returns not supported when connection is slow-2g', () => {
			Object.defineProperty( navigator, 'connection', {
				value: {
					saveData: false,
					effectiveType: 'slow-2g',
				},
				configurable: true,
			} );

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toContain( 'too slow' );
		} );

		it( 'returns not supported when CSP blocks blob workers', () => {
			// Simulate CSP blocking blob URL workers by throwing a SecurityError.
			global.Worker = class ThrowingWorker {
				constructor() {
					throw new DOMException(
						"Refused to create a worker from 'blob:...' because it violates the Content Security Policy directive: \"worker-src 'self'\".",
						'SecurityError'
					);
				}
			} as unknown as typeof Worker;

			const result = detectClientSideMediaSupport();

			expect( result.supported ).toBe( false );
			expect( result.reason ).toContain( 'Content Security Policy' );
			expect( result.reason ).toContain( 'worker-src' );
		} );

		it( 'caches the result', () => {
			const result1 = detectClientSideMediaSupport();
			expect( result1.supported ).toBe( true );

			// Now set WebAssembly to undefined - cached result should still be returned.
			// @ts-ignore - Intentionally setting WebAssembly to undefined for testing.
			global.WebAssembly = undefined;

			const result2 = detectClientSideMediaSupport();
			expect( result2.supported ).toBe( true );
			expect( result2 ).toBe( result1 ); // Same object reference.
		} );
	} );

	describe( 'isClientSideMediaSupported', () => {
		it( 'returns true when all features are available', () => {
			expect( isClientSideMediaSupported() ).toBe( true );
		} );

		it( 'returns false when features are unavailable', () => {
			// @ts-ignore - Intentionally setting WebAssembly to undefined for testing.
			global.WebAssembly = undefined;

			expect( isClientSideMediaSupported() ).toBe( false );
		} );
	} );

	describe( 'isHeicCanvasSupported', () => {
		const originalCreateImageBitmap =
			global.createImageBitmap as typeof createImageBitmap;
		const originalOffscreenCanvas =
			global.OffscreenCanvas as typeof OffscreenCanvas;

		afterEach( () => {
			// Restore globals after each test.
			if ( originalCreateImageBitmap !== undefined ) {
				global.createImageBitmap =
					originalCreateImageBitmap as typeof createImageBitmap;
			} else {
				// @ts-ignore
				delete global.createImageBitmap;
			}
			if ( originalOffscreenCanvas !== undefined ) {
				global.OffscreenCanvas =
					originalOffscreenCanvas as typeof OffscreenCanvas;
			} else {
				// @ts-ignore
				delete global.OffscreenCanvas;
			}
		} );

		it( 'returns true when both createImageBitmap and OffscreenCanvas are available', () => {
			global.createImageBitmap =
				jest.fn() as unknown as typeof createImageBitmap;
			global.OffscreenCanvas =
				jest.fn() as unknown as typeof OffscreenCanvas;

			expect( isHeicCanvasSupported() ).toBe( true );
		} );

		it( 'returns false when createImageBitmap is unavailable', () => {
			// @ts-ignore
			delete global.createImageBitmap;
			global.OffscreenCanvas =
				jest.fn() as unknown as typeof OffscreenCanvas;

			expect( isHeicCanvasSupported() ).toBe( false );
		} );

		it( 'returns false when OffscreenCanvas is unavailable', () => {
			global.createImageBitmap =
				jest.fn() as unknown as typeof createImageBitmap;
			// @ts-ignore
			delete global.OffscreenCanvas;

			expect( isHeicCanvasSupported() ).toBe( false );
		} );

		it( 'returns false when both are unavailable', () => {
			// @ts-ignore
			delete global.createImageBitmap;
			// @ts-ignore
			delete global.OffscreenCanvas;

			expect( isHeicCanvasSupported() ).toBe( false );
		} );
	} );

	describe( 'clearFeatureDetectionCache', () => {
		it( 'clears the cached result', () => {
			const result1 = detectClientSideMediaSupport();
			expect( result1.supported ).toBe( true );

			// Clear cache and set WebAssembly to undefined.
			clearFeatureDetectionCache();
			// @ts-ignore - Intentionally setting WebAssembly to undefined for testing.
			global.WebAssembly = undefined;

			const result2 = detectClientSideMediaSupport();
			expect( result2.supported ).toBe( false );
		} );
	} );

	describe( 'exceedsClientProcessingMemory', () => {
		it( 'allows typical images', () => {
			expect(
				exceedsClientProcessingMemory( {
					width: 4000,
					height: 3000,
					interlaced: false,
				} )
			).toBe( false );
			expect(
				exceedsClientProcessingMemory( {
					width: 4000,
					height: 3000,
					interlaced: true,
				} )
			).toBe( false );
		} );

		it( 'gates the reported interlaced flower.jpg (20000x11857)', () => {
			expect(
				exceedsClientProcessingMemory( {
					width: 20000,
					height: 11857,
					interlaced: true,
				} )
			).toBe( true );
		} );

		it( 'applies a tighter budget to interlaced images', () => {
			// ~150 MP: over the ~0.5 GiB interlaced budget but under the
			// ~0.9 GiB baseline budget.
			const dimensions = { width: 15000, height: 10000 };

			expect(
				exceedsClientProcessingMemory( {
					...dimensions,
					interlaced: true,
				} )
			).toBe( true );
			expect(
				exceedsClientProcessingMemory( {
					...dimensions,
					interlaced: false,
				} )
			).toBe( false );
		} );

		it( 'gates extremely large baseline images', () => {
			// ~300 MP exceeds even the generous baseline budget.
			expect(
				exceedsClientProcessingMemory( {
					width: 20000,
					height: 15000,
					interlaced: false,
				} )
			).toBe( true );
		} );
	} );
} );
