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

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

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

function makeFakeBinding({sources = [], availability = {sck: {supported: true}, screenPermission: 'authorized'}} = {}) {
	const calls = [];
	const frameSinkHandleCalls = [];
	const natives = [];
	const frameSinkDiagnostics = {
		accepted: 0,
		coalesced: 0,
		rejected: 0,
		mediaFramesDroppedWithoutSink: 0,
	};
	class FakeNative {
		constructor() {
			this.lifecycleCallback = undefined;
			this.stopCount = 0;
			natives.push(this);
		}

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

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

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

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

		getFrameSinkDiagnostics() {
			return {...frameSinkDiagnostics};
		}
	}
	return {
		binding: {
			ScreenCapture: FakeNative,
			listSources: async () => sources,
			getBackendAvailability: async () => availability,
			getBackendInfo: () => ({
				backend: 'mac-screen-capture',
				supported: true,
				reason: '',
				minMacosVersion: '12.3',
				detectedMacosVersion: '14.0',
				sckAvailable: true,
			}),
		},
		calls,
		frameSinkHandleCalls,
		frameSinkDiagnostics,
		natives,
	};
}

afterEach(() => {
	macScreenCapture.__setBindingForTests(null);
});

describe('mac-screen-capture loader wrapper', () => {
	test('forwards source id, kind, and dimensions to native binding', async () => {
		const {binding, calls} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.ScreenCapture({
			sourceId: '12345',
			sourceKind: 'window',
			width: 1280,
			height: 720,
			frameRate: 30,
			colorRange: 'full',
			colorSpace: 'rec709',
			showCursorClicks: true,
			captureRect: {x: 10, y: 20, width: 300, height: 200},
		});
		capture.on('error', () => {});
		const result = await capture.start();
		assert.deepEqual(calls, [
			{
				sourceId: '12345',
				sourceKind: 'window',
				width: 1280,
				height: 720,
				frameRate: 30,
				captureId: undefined,
				captureOptions: {
					colorRange: 'full',
					colorSpace: 'rec709',
					showCursorClicks: true,
					captureRect: {x: 10, y: 20, width: 300, height: 200},
				},
			},
		]);
		assert.equal(result.pixelFormat, 'nv12');
		assert.equal(result.width, 1280);
		assert.equal(result.height, 720);
	});
	test('defaults sourceKind to screen and frameRate to 30', async () => {
		const {binding, calls} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
		capture.on('error', () => {});
		await capture.start();
		assert.equal(calls[0].sourceKind, 'screen');
		assert.equal(calls[0].frameRate, 30);
	});
	test('forwards display and window sources from native binding without rewriting ids', async () => {
		const {binding} = makeFakeBinding({
			sources: [
				{kind: 'screen', id: 'display:69733632', name: 'Studio Display', width: 5120, height: 2880},
				{
					kind: 'window',
					id: 'window:4242',
					name: 'Fluxer',
					width: 1440,
					height: 900,
					appName: 'Fluxer',
					bundleId: 'app.fluxer.desktop',
					targetPid: 1234,
				},
			],
		});
		macScreenCapture.__setBindingForTests(binding);

		const sources = await macScreenCapture.listSources();

		assert.deepEqual(sources, [
			{kind: 'screen', id: 'display:69733632', name: 'Studio Display', width: 5120, height: 2880},
			{
				kind: 'window',
				id: 'window:4242',
				name: 'Fluxer',
				width: 1440,
				height: 900,
				appName: 'Fluxer',
				bundleId: 'app.fluxer.desktop',
				targetPid: 1234,
			},
		]);
	});
	test('reports ScreenCaptureKit support and permission from native binding', async () => {
		const {binding} = makeFakeBinding({
			availability: {
				sck: {supported: true, macosVersion: '15.0'},
				screenPermission: 'authorized',
			},
		});
		macScreenCapture.__setBindingForTests(binding);

		assert.deepEqual(await macScreenCapture.getBackendAvailability(), {
			sck: {supported: true, macosVersion: '15.0'},
			screenPermission: 'authorized',
		});
	});
	test('installs a native frame sink handle once before start', async () => {
		const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const frameSinkHandle = {native: true};
		const capture = new macScreenCapture.ScreenCapture({
			sourceId: '12345',
			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', async () => {
		const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.ScreenCapture({
			sourceId: '12345',
			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('frame sink diagnostics are forwarded by the wrapper', () => {
		const {binding, frameSinkDiagnostics} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});

		frameSinkDiagnostics.accepted = 5;
		frameSinkDiagnostics.coalesced = 1;
		frameSinkDiagnostics.rejected = 2;
		frameSinkDiagnostics.mediaFramesDroppedWithoutSink = 3;

		assert.deepEqual(capture.getFrameSinkDiagnostics(), {
			accepted: 5,
			coalesced: 1,
			rejected: 2,
			mediaFramesDroppedWithoutSink: 3,
		});
	});
	test('emits closed once when native lifecycle closes and stop is called later', async () => {
		const {binding, natives} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.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 error emits Error event', async () => {
		const {binding, natives} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
		const errors = [];
		capture.on('error', (err) => errors.push(err));
		natives[0].lifecycleCallback('error', 'permission lost mid-stream');
		assert.equal(errors.length, 1);
		assert.equal(errors[0].message, 'permission lost mid-stream');
	});
	test('lifecycle diagnostic emits diagnostic event', () => {
		const {binding, natives} = makeFakeBinding();
		macScreenCapture.__setBindingForTests(binding);
		const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
		const diagnostics = [];
		capture.on('diagnostic', (message) => diagnostics.push(message));
		natives[0].lifecycleCallback('diagnostic', 'frame sink rejected a frame');
		assert.deepEqual(diagnostics, ['frame sink rejected a frame']);
	});
});
