// SPDX-License-Identifier: AGPL-3.0-or-later

import assert from 'node:assert/strict';
import {existsSync} from 'node:fs';
import {createRequire} from 'node:module';
import {afterEach, describe, test} from 'node:test';

const require = createRequire(import.meta.url);
const winGameCapture = require('./index.js');

const startupSupported = winGameCapture.isSupported();
const startupAvailability = winGameCapture.getAvailability();
const realBindingSkip = startupSupported
	? false
	: 'no native binding loaded at startup (binding-less platform / unbuilt addon)';

const hasBindingHook = typeof winGameCapture.__setBindingForTests === 'function';
const injectionSkip = hasBindingHook ? false : 'no __setBindingForTests hook exported';

if (!hasBindingHook) {
	console.warn(
		'[index.test.mjs] __setBindingForTests not present on index.js; ' +
			'skipping binding-injection tests and running only the no-binding tests. ' +
			'This should only happen when testing an older or stripped loader.',
	);
}

function makeFakeBinding() {
	const calls = [];
	const frameSinkHandleCalls = [];
	const priorityCalls = [];
	const natives = [];
	const diagnostics = {
		state: 1,
		apiType: 5,
		transport: 0,
		fallbackReason: 0,
		captureFlags: 0,
		width: 1920,
		height: 1080,
		dxgiFormat: 87,
		frameCounter: 42,
		droppedFrameCounter: 0,
		lastPresentTimestampUs: 123456,
		lastError: 0,
		requestedInjectionMethod: 'auto',
		injectionMethod: 'remote-thread',
		activeStrategy: 'game-hook',
		lastFallbackReason: '',
		startOptions: {
			colorRange: 'full',
			colorSpace: 'rec709',
			showCursorClicks: true,
			captureRect: {x: 10, y: 20, width: 300, height: 200},
			unsupportedOptions: ['showCursorClicks', 'captureRect', 'colorRange', 'colorSpace'],
		},
		frameSinkAccepted: 0,
		frameSinkCoalesced: 0,
		frameSinkRejected: 0,
		mediaFramesDroppedWithoutSink: 0,
		cpuFallbackFramesDropped: 0,
	};
	const encoderDiagnostics = {
		attached: false,
		width: 0,
		height: 0,
		capacity: 0,
		framesSubmitted: 0,
		framesDropped: 0,
		ringFullEvents: 0,
		failedBlits: 0,
	};
	const frameSinkDiagnostics = {
		accepted: 0,
		coalesced: 0,
		rejected: 0,
		mediaFramesDroppedWithoutSink: 0,
		cpuFallbackFramesDropped: 0,
	};

	class FakeNative {
		constructor() {
			this.lifecycleCallback = undefined;
			this.stopCount = 0;
			natives.push(this);
		}

		setLifecycleCallback(callback) {
			this.lifecycleCallback = callback;
		}

		setFrameSinkHandle(handle) {
			frameSinkHandleCalls.push(handle);
		}

		start(
			sourceId,
			sourceKind,
			width,
			height,
			frameRate,
			hookDllPath,
			hookDllPathX86,
			injectionMethod,
			captureId,
			captureOptions,
		) {
			calls.push({
				sourceId,
				sourceKind,
				width,
				height,
				frameRate,
				hookDllPath,
				hookDllPathX86,
				injectionMethod,
				captureId,
				captureOptions,
			});
			return {
				width: width || 1920,
				height: height || 1080,
				frameRate: frameRate || 30,
				pixelFormat: 'bgra',
			};
		}

		stop() {
			this.stopCount += 1;
		}

		getDiagnostics() {
			return diagnostics;
		}

		attachEncoder(width, height) {
			encoderDiagnostics.attached = true;
			encoderDiagnostics.width = width;
			encoderDiagnostics.height = height;
			encoderDiagnostics.capacity = 8;
		}

		detachEncoder() {
			encoderDiagnostics.attached = false;
		}

		isEncoderAttached() {
			return encoderDiagnostics.attached;
		}

		encoderRingFullCount() {
			return encoderDiagnostics.ringFullEvents;
		}

		getEncoderAttachDiagnostics() {
			if (!encoderDiagnostics.attached) return null;
			return {...encoderDiagnostics};
		}

		getFrameSinkDiagnostics() {
			return {...frameSinkDiagnostics};
		}
	}

	return {
		binding: {
			ScreenCapture: FakeNative,
			isSupported: () => true,
			getAvailability: () => ({available: true, backend: 'windows-game-capture'}),
			listSources: () => [
				{
					kind: 'screen',
					id: 'screen:0:0',
					name: 'Display 1',
					width: 2560,
					height: 1440,
				},
				{
					kind: 'window',
					id: 'window:5050:0',
					name: 'Fixture',
					width: 1280,
					height: 720,
					targetPid: 4242,
				},
				{
					kind: 'browser',
					id: 'screen:1:0',
					name: 'unsupported shape',
				},
			],
			elevateGpuSchedulingPriority: (processId, priorityClass) => {
				priorityCalls.push({type: 'elevate', processId, priorityClass});
			},
			restoreGpuSchedulingPriority: (processId) => {
				priorityCalls.push({type: 'restore', processId});
			},
		},
		calls,
		frameSinkHandleCalls,
		priorityCalls,
		natives,
		diagnostics,
		encoderDiagnostics,
		frameSinkDiagnostics,
	};
}

afterEach(() => {
	if (hasBindingHook) {
		winGameCapture.__setBindingForTests(null);
	}
});

describe('win-game-capture loader wrapper -- binding-absent fallback path', () => {
	function forceNoBinding() {
		if (hasBindingHook) winGameCapture.__setBindingForTests(null);
	}

	test(
		'isSupported() is false when no binding is loaded',
		{skip: hasBindingHook ? false : 'no __setBindingForTests hook'},
		() => {
			forceNoBinding();
			assert.equal(winGameCapture.isSupported(), false);
		},
	);

	test(
		'getAvailability() returns the {available:false, ...} shape when no binding is loaded',
		{skip: hasBindingHook ? false : 'no __setBindingForTests hook'},
		() => {
			forceNoBinding();
			const availability = winGameCapture.getAvailability();
			assert.equal(availability.available, false);
			assert.equal(availability.backend, 'windows-game-capture');
			assert.equal(typeof availability.reason, 'string');
		},
	);

	test(
		'constructing ScreenCapture without a binding throws',
		{skip: hasBindingHook ? false : 'no __setBindingForTests hook'},
		() => {
			forceNoBinding();
			assert.throws(() => new winGameCapture.ScreenCapture({sourceId: '1'}));
			try {
				new winGameCapture.ScreenCapture({sourceId: '1'});
				assert.fail('expected ScreenCapture constructor to throw without a binding');
			} catch (error) {
				assert.ok(error instanceof Error);
			}
		},
	);
});

describe('win-game-capture loader wrapper -- arch path resolvers (platform-portable)', () => {
	test('resolveGameHookPath() is null or the host-arch hook path', () => {
		const r = winGameCapture.resolveGameHookPath();
		assert.ok(
			r === null || (typeof r === 'string' && /fluxer-game-hook\.win32-(x64|ia32|arm64)-msvc\.dll$/.test(r)),
			`unexpected resolveGameHookPath(): ${r}`,
		);
	});

	test('resolveGameHookPathX86() is null or the ia32 hook path', () => {
		const r = winGameCapture.resolveGameHookPathX86();
		assert.ok(
			r === null || (typeof r === 'string' && r.endsWith('fluxer-game-hook.win32-ia32-msvc.dll')),
			`unexpected resolveGameHookPathX86(): ${r}`,
		);
	});

	test('resolveVulkanLayerManifestPath() is null or the host-arch layer manifest path', () => {
		const r = winGameCapture.resolveVulkanLayerManifestPath();
		assert.ok(
			r === null || (typeof r === 'string' && /fluxer-vulkan-layer\.win32-(x64|ia32|arm64)-msvc\.json$/.test(r)),
			`unexpected resolveVulkanLayerManifestPath(): ${r}`,
		);
	});
});

describe('win-game-capture loader wrapper -- real native binding (built Windows box)', () => {
	test('getAvailability() reports available with the windows-game-capture backend', {skip: realBindingSkip}, () => {
		assert.equal(startupAvailability.available, true);
		assert.equal(startupAvailability.backend, 'windows-game-capture');
	});

	test('resolveGameHookPath() points at an existing host-arch hook DLL', {skip: realBindingSkip}, () => {
		const r = winGameCapture.resolveGameHookPath();
		assert.equal(typeof r, 'string', 'expected a hook path on a built Windows box');
		assert.ok(existsSync(r), `hook DLL should exist on disk: ${r}`);
	});

	test('resolveVulkanLayerManifestPath() points at an existing layer manifest', {skip: realBindingSkip}, () => {
		const r = winGameCapture.resolveVulkanLayerManifestPath();
		assert.equal(typeof r, 'string', 'expected a Vulkan layer manifest path on a built Windows box');
		assert.ok(existsSync(r), `Vulkan layer manifest should exist on disk: ${r}`);
	});
});

describe('win-game-capture loader wrapper -- injected fake binding', () => {
	test(
		'listSources() forwards sanitized screen/window sources from the native binding',
		{skip: injectionSkip},
		async () => {
			const {binding} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const sources = await winGameCapture.listSources();
			assert.deepEqual(sources, [
				{
					kind: 'screen',
					id: 'screen:0:0',
					name: 'Display 1',
					width: 2560,
					height: 1440,
					targetPid: undefined,
				},
				{
					kind: 'window',
					id: 'window:5050:0',
					name: 'Fixture',
					width: 1280,
					height: 720,
					targetPid: 4242,
				},
			]);
		},
	);

	test(
		'start() forwards sourceId/kind/dims/frameRate and BOTH hook paths (6th + 7th args)',
		{skip: injectionSkip},
		async () => {
			const {binding, calls} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({
				sourceId: '987654',
				sourceKind: 'game',
				width: 1600,
				height: 900,
				frameRate: 60,
				hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
				hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
				captureId: 'capture-1',
				colorRange: 'full',
				colorSpace: 'rec709',
				showCursorClicks: true,
				captureRect: {x: 10, y: 20, width: 300, height: 200},
			});
			capture.on('error', () => {});
			const result = await capture.start();
			assert.equal(calls.length, 1);
			assert.deepEqual(calls[0], {
				sourceId: '987654',
				sourceKind: 'game',
				width: 1600,
				height: 900,
				frameRate: 60,
				hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
				hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
				injectionMethod: undefined,
				captureId: 'capture-1',
				captureOptions: {
					colorRange: 'full',
					colorSpace: 'rec709',
					showCursorClicks: true,
					captureRect: {x: 10, y: 20, width: 300, height: 200},
				},
			});
			assert.equal(result.pixelFormat, 'bgra');
			assert.equal(result.width, 1600);
			assert.equal(result.height, 900);
			assert.equal(result.frameRate, 60);
		},
	);

	test(
		'start() forwards the injectionMethod option as the 8th arg for game capture',
		{skip: injectionSkip},
		async () => {
			const {binding, calls} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({
				sourceId: '555',
				sourceKind: 'game',
				hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
				hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
				injectionMethod: 'set-windows-hook',
			});
			capture.on('error', () => {});
			await capture.start();
			assert.equal(calls.length, 1);
			assert.equal(calls[0].injectionMethod, 'set-windows-hook');
		},
	);

	test('window sourceKind does not forward the injectionMethod', {skip: injectionSkip}, async () => {
		const {binding, calls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({
			sourceId: '42',
			sourceKind: 'window',
			injectionMethod: 'set-windows-hook',
		});
		capture.on('error', () => {});
		await capture.start();
		assert.equal(calls[0].injectionMethod, undefined);
	});

	test('window sourceKind does not forward hook paths', {skip: injectionSkip}, async () => {
		const {binding, calls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({
			sourceId: '42',
			sourceKind: 'window',
			hookDllPath: 'C:/hooks/primary.dll',
			hookDllPathX86: 'C:/hooks/x86.dll',
		});
		capture.on('error', () => {});
		await capture.start();
		assert.equal(calls[0].sourceKind, 'window');
		assert.equal(calls[0].hookDllPath, undefined);
		assert.equal(calls[0].hookDllPathX86, undefined);
	});

	test('screen sourceKind does not forward hook paths or the injectionMethod', {skip: injectionSkip}, async () => {
		const {binding, calls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({
			sourceId: 'screen:0:0',
			sourceKind: 'screen',
			hookDllPath: 'C:/hooks/primary.dll',
			hookDllPathX86: 'C:/hooks/x86.dll',
			injectionMethod: 'set-windows-hook',
		});
		capture.on('error', () => {});
		await capture.start();
		assert.equal(calls[0].sourceKind, 'screen');
		assert.equal(calls[0].hookDllPath, undefined);
		assert.equal(calls[0].hookDllPathX86, undefined);
		assert.equal(calls[0].injectionMethod, undefined);
	});

	test('getDiagnostics() exposes native start option state', {skip: injectionSkip}, () => {
		const {binding} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '42', sourceKind: 'window'});
		const diagnostics = capture.getDiagnostics();
		assert.deepEqual(diagnostics.startOptions, {
			colorRange: 'full',
			colorSpace: 'rec709',
			showCursorClicks: true,
			captureRect: {x: 10, y: 20, width: 300, height: 200},
			unsupportedOptions: ['showCursorClicks', 'captureRect', 'colorRange', 'colorSpace'],
		});
	});

	test('encoder attachment diagnostics are forwarded by the wrapper', {skip: injectionSkip}, () => {
		const {binding, encoderDiagnostics} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '42', sourceKind: 'window'});

		assert.equal(capture.isEncoderAttached(), false);
		assert.equal(capture.encoderRingFullCount(), 0);
		assert.equal(capture.getEncoderAttachDiagnostics(), null);

		capture.attachEncoder(1280, 720);
		encoderDiagnostics.framesSubmitted = 3;
		encoderDiagnostics.framesDropped = 1;
		encoderDiagnostics.ringFullEvents = 1;
		encoderDiagnostics.failedBlits = 0;

		assert.equal(capture.isEncoderAttached(), true);
		assert.equal(capture.encoderRingFullCount(), 1);
		assert.deepEqual(capture.getEncoderAttachDiagnostics(), {
			attached: true,
			width: 1280,
			height: 720,
			capacity: 8,
			framesSubmitted: 3,
			framesDropped: 1,
			ringFullEvents: 1,
			failedBlits: 0,
		});

		capture.detachEncoder();
		assert.equal(capture.isEncoderAttached(), false);
	});

	test('frame sink diagnostics are forwarded by the wrapper', {skip: injectionSkip}, () => {
		const {binding, frameSinkDiagnostics} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '42', sourceKind: 'window'});

		frameSinkDiagnostics.accepted = 7;
		frameSinkDiagnostics.coalesced = 2;
		frameSinkDiagnostics.rejected = 1;
		frameSinkDiagnostics.mediaFramesDroppedWithoutSink = 3;
		frameSinkDiagnostics.cpuFallbackFramesDropped = 4;

		assert.deepEqual(capture.getFrameSinkDiagnostics(), {
			accepted: 7,
			coalesced: 2,
			rejected: 1,
			mediaFramesDroppedWithoutSink: 3,
			cpuFallbackFramesDropped: 4,
		});
	});

	test('installs a native frame sink handle once before start', {skip: injectionSkip}, async () => {
		const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const frameSinkHandle = {native: true};
		const capture = new winGameCapture.ScreenCapture({
			sourceId: '42',
			sourceKind: 'window',
			frameSinkHandle,
			nativeFrameSinkRequired: true,
		});
		capture.on('error', () => {});

		await capture.start();

		assert.deepEqual(frameSinkHandleCalls, [frameSinkHandle]);
		assert.equal(calls.length, 1);
	});

	test(
		'fails before native start when a native frame sink is required but missing',
		{skip: injectionSkip},
		async () => {
			const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({
				sourceId: '42',
				sourceKind: 'window',
				nativeFrameSinkRequired: true,
			});
			capture.on('error', () => {});

			await assert.rejects(() => capture.start(), /native frame sink handle is required/i);
			assert.deepEqual(frameSinkHandleCalls, []);
			assert.deepEqual(calls, []);
		},
	);

	test(
		'game sourceKind without a hook path fails closed without WGC/browser fallback',
		{skip: injectionSkip},
		async () => {
			const {binding, calls} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({
				sourceId: '7',
				sourceKind: 'game',
				hookDllPath: '',
				hookDllPathX86: '',
			});
			capture.on('error', () => {});
			await assert.rejects(() => capture.start(), /Game capture hook unavailable/);
			assert.equal(calls.length, 0, 'native start must not be called when the game hook is unavailable');
		},
	);

	test('lifecycle "error" emits an Error event', {skip: injectionSkip}, () => {
		const {binding, natives} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
		const errors = [];
		capture.on('error', (err) => errors.push(err));
		natives[0].lifecycleCallback('error', 'DXGI device removed');
		assert.equal(errors.length, 1);
		assert.ok(errors[0] instanceof Error);
		assert.equal(errors[0].message, 'DXGI device removed');
	});

	test('lifecycle "closed" stops the native capture and emits closed once', {skip: injectionSkip}, async () => {
		const {binding, natives} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
		let closed = 0;
		capture.on('closed', () => {
			closed += 1;
		});
		natives[0].lifecycleCallback('closed', '');
		await Promise.resolve();
		await capture.stop();
		assert.equal(closed, 1);
		assert.equal(natives[0].stopCount, 1);
	});

	test('non-fatal "stalled" lifecycle re-emits as a stalled event WITHOUT stopping', {skip: injectionSkip}, () => {
		const {binding, natives} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
		const stalls = [];
		let closed = 0;
		capture.on('stalled', (message) => stalls.push(message));
		capture.on('closed', () => {
			closed += 1;
		});
		natives[0].lifecycleCallback('stalled', 'frame counter frozen while focused');
		assert.deepEqual(stalls, ['frame counter frozen while focused']);
		assert.equal(natives[0].stopCount, 0);
		assert.equal(closed, 0);
	});

	test(
		'non-fatal "diagnostic" lifecycle re-emits as a diagnostic event WITHOUT stopping',
		{skip: injectionSkip},
		() => {
			const {binding, natives} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
			const diags = [];
			let closed = 0;
			capture.on('diagnostic', (message) => diags.push(message));
			capture.on('closed', () => {
				closed += 1;
			});
			natives[0].lifecycleCallback('diagnostic', 'fell back to CPU readback');
			assert.deepEqual(diags, ['fell back to CPU readback']);
			assert.equal(natives[0].stopCount, 0);
			assert.equal(closed, 0);
		},
	);

	test(
		'lifecycle "error" in the real napi [type, message] array shape emits an Error event',
		{skip: injectionSkip},
		() => {
			const {binding, natives} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
			const errors = [];
			capture.on('error', (err) => errors.push(err));
			natives[0].lifecycleCallback(['error', 'DXGI device removed']);
			assert.equal(errors.length, 1);
			assert.ok(errors[0] instanceof Error);
			assert.equal(errors[0].message, 'DXGI device removed');
		},
	);

	test(
		'lifecycle "closed" in the napi array shape stops the native capture and emits closed once',
		{skip: injectionSkip},
		async () => {
			const {binding, natives} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
			let closed = 0;
			capture.on('closed', () => {
				closed += 1;
			});
			natives[0].lifecycleCallback(['closed', '']);
			await Promise.resolve();
			await capture.stop();
			assert.equal(closed, 1);
			assert.equal(natives[0].stopCount, 1);
		},
	);

	test(
		'lifecycle "stalled" in the napi array shape re-emits as a stalled event WITHOUT stopping',
		{skip: injectionSkip},
		() => {
			const {binding, natives} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
			const stalls = [];
			let closed = 0;
			capture.on('stalled', (message) => stalls.push(message));
			capture.on('closed', () => {
				closed += 1;
			});
			natives[0].lifecycleCallback(['stalled', 'frame counter frozen while focused']);
			assert.deepEqual(stalls, ['frame counter frozen while focused']);
			assert.equal(natives[0].stopCount, 0);
			assert.equal(closed, 0);
		},
	);

	test(
		'lifecycle "diagnostic" (injected via <method>) in the napi array shape re-emits WITHOUT stopping',
		{skip: injectionSkip},
		() => {
			const {binding, natives} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
			const diags = [];
			let closed = 0;
			capture.on('diagnostic', (message) => diags.push(message));
			capture.on('closed', () => {
				closed += 1;
			});
			natives[0].lifecycleCallback(['diagnostic', 'game capture injected via set-windows-hook']);
			assert.deepEqual(diags, ['game capture injected via set-windows-hook']);
			assert.equal(natives[0].stopCount, 0);
			assert.equal(closed, 0);
		},
	);

	test('getDiagnostics() passes the native snapshot through', {skip: injectionSkip}, () => {
		const {binding, diagnostics} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
		const snapshot = capture.getDiagnostics();
		assert.deepEqual(snapshot, diagnostics);
	});

	test('getDiagnostics() surfaces the activeStrategy + lastFallbackReason fields', {skip: injectionSkip}, () => {
		const {binding, diagnostics} = makeFakeBinding();
		diagnostics.activeStrategy = 'dxgi-duplication';
		diagnostics.lastFallbackReason =
			'game-hook capture could not inject its capture hook; switching to dxgi-duplication capture';
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
		const snapshot = capture.getDiagnostics();
		assert.equal(snapshot.activeStrategy, 'dxgi-duplication');
		assert.match(snapshot.lastFallbackReason, /switching to dxgi-duplication/);
	});

	test('elevateGpuSchedulingPriority forwards the optional realtime priority class', {skip: injectionSkip}, () => {
		const {binding, priorityCalls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234, 'realtime'), true);
		assert.deepEqual(priorityCalls, [{type: 'elevate', processId: 1234, priorityClass: 'realtime'}]);
	});

	test('elevateGpuSchedulingPriority canonicalizes the real-time priority alias', {skip: injectionSkip}, () => {
		const {binding, priorityCalls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234, 'real-time'), true);
		assert.deepEqual(priorityCalls, [{type: 'elevate', processId: 1234, priorityClass: 'realtime'}]);
	});

	test('elevateGpuSchedulingPriority preserves the existing default priority class', {skip: injectionSkip}, () => {
		const {binding, priorityCalls} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234), true);
		assert.deepEqual(priorityCalls, [{type: 'elevate', processId: 1234, priorityClass: undefined}]);
	});

	test(
		'elevateGpuSchedulingPriority rejects unsupported priority classes before binding calls',
		{skip: injectionSkip},
		() => {
			const {binding, priorityCalls} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			assert.equal(winGameCapture.elevateGpuSchedulingPriority(1234, 'normal'), false);
			assert.deepEqual(priorityCalls, []);
		},
	);

	test('lifecycle fallback "error" (next-strategy=...) re-emits as a fatal error event', {skip: injectionSkip}, () => {
		const {binding, natives} = makeFakeBinding();
		winGameCapture.__setBindingForTests(binding);
		const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
		const errors = [];
		capture.on('error', (err) => errors.push(err));
		natives[0].lifecycleCallback([
			'error',
			'fallback: game-hook -> dxgi-duplication (game-hook capture could not inject its capture hook) [next-strategy=dxgi-duplication]',
		]);
		assert.equal(errors.length, 1);
		assert.ok(errors[0] instanceof Error);
		assert.equal(winGameCapture.parseFallbackRecommendation(errors[0].message), 'dxgi-duplication');
	});

	test(
		'lifecycle fallback "diagnostic" (upgrade, next-strategy=...) re-emits WITHOUT stopping',
		{skip: injectionSkip},
		() => {
			const {binding, natives} = makeFakeBinding();
			winGameCapture.__setBindingForTests(binding);
			const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
			const diags = [];
			let closed = 0;
			capture.on('diagnostic', (message) => diags.push(message));
			capture.on('closed', () => {
				closed += 1;
			});
			natives[0].lifecycleCallback([
				'diagnostic',
				'upgrade: window-gdi -> dxgi-duplication (window-gdi has been stable) [next-strategy=dxgi-duplication]',
			]);
			assert.equal(diags.length, 1);
			assert.equal(winGameCapture.parseFallbackRecommendation(diags[0]), 'dxgi-duplication');
			assert.equal(natives[0].stopCount, 0);
			assert.equal(closed, 0);
		},
	);
});

describe('win-game-capture loader wrapper -- parseFallbackRecommendation', () => {
	test('extracts the recommended strategy from a transition error message', () => {
		assert.equal(
			winGameCapture.parseFallbackRecommendation(
				'fallback: game-hook -> window-gdi (reason) [next-strategy=window-gdi]',
			),
			'window-gdi',
		);
	});

	test('returns "none" for an exhausted give-up message', () => {
		assert.equal(
			winGameCapture.parseFallbackRecommendation(
				'fallback exhausted: window-gdi was the last resort [next-strategy=none]',
			),
			'none',
		);
	});

	test('returns null for an ordinary error with no recommendation', () => {
		assert.equal(winGameCapture.parseFallbackRecommendation('DXGI device removed'), null);
		assert.equal(winGameCapture.parseFallbackRecommendation(undefined), null);
		assert.equal(winGameCapture.parseFallbackRecommendation(''), null);
	});

	test('returns null for an unrecognised strategy token', () => {
		assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=teleporter]'), null);
	});

	test('accepts the WGC fallback strategy token', () => {
		assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=wgc]'), 'wgc');
		assert.equal(
			winGameCapture.parseFallbackRecommendation(
				'fallback: game-hook -> wgc (game-hook capture could not inject its capture hook) [next-strategy=wgc]',
			),
			'wgc',
		);
	});

	test('rejects browser fallback strategy tokens', () => {
		assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=browser-display]'), null);
		assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=desktop-capturer]'), null);
	});
});
