import { Buffer } from 'node:buffer';
import process from 'node:process';
import { PassThrough, Readable } from 'node:stream';
import { opus, VolumeTransformer } from 'prism-media';
import { describe, test, expect, vitest, type MockedFunction, beforeAll, beforeEach } from 'vitest';
import { SILENCE_FRAME } from '../src/audio/AudioPlayer';
import { AudioResource, createAudioResource, NO_CONSTRAINT, VOLUME_CONSTRAINT } from '../src/audio/AudioResource';
import { findPipeline as _findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph';

vitest.mock('../src/audio/TransformerGraph');

async function wait() {
	// eslint-disable-next-line no-promise-executor-return
	return new Promise((resolve) => process.nextTick(resolve));
}

async function started(resource: AudioResource) {
	while (!resource.started) {
		await wait();
	}

	return resource;
}

const findPipeline = _findPipeline as unknown as MockedFunction<typeof _findPipeline>;

beforeAll(() => {
	// @ts-expect-error: No type
	findPipeline.mockImplementation((from: StreamType, constraint: (path: Edge[]) => boolean) => {
		const base = [
			{
				cost: 1,
				transformer: () => new PassThrough(),
				type: TransformerType.FFmpegPCM,
			},
		];
		if (constraint === VOLUME_CONSTRAINT) {
			base.push({
				cost: 1,
				// Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer
				transformer: () => new VolumeTransformer({ type: 's16le' } as any),
				type: TransformerType.InlineVolume,
			});
		}

		return base as any[];
	});
});

beforeEach(() => {
	findPipeline.mockClear();
});

describe('createAudioResource', () => {
	test('Creates a resource from string path', () => {
		const resource = createAudioResource('mypath.mp3');
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, NO_CONSTRAINT);
		expect(resource.volume).toBeUndefined();
	});

	test('Creates a resource from string path (volume)', () => {
		const resource = createAudioResource('mypath.mp3', { inlineVolume: true });
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, VOLUME_CONSTRAINT);
		expect(resource.volume).toBeInstanceOf(VolumeTransformer);
	});

	test('Only infers type if not explicitly given', () => {
		const resource = createAudioResource(new opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }), {
			inputType: StreamType.Arbitrary,
		});
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, NO_CONSTRAINT);
		expect(resource.volume).toBeUndefined();
	});

	test('Infers from opus.Encoder', () => {
		const resource = createAudioResource(new opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }), {
			inlineVolume: true,
		});
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Opus, VOLUME_CONSTRAINT);
		expect(resource.volume).toBeInstanceOf(VolumeTransformer);
		expect(resource.encoder).toBeInstanceOf(opus.Encoder);
	});

	test('Infers from opus.OggDemuxer', () => {
		const resource = createAudioResource(new opus.OggDemuxer());
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Opus, NO_CONSTRAINT);
		expect(resource.volume).toBeUndefined();
		expect(resource.encoder).toBeUndefined();
	});

	test('Infers from opus.WebmDemuxer', () => {
		const resource = createAudioResource(new opus.WebmDemuxer());
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Opus, NO_CONSTRAINT);
		expect(resource.volume).toBeUndefined();
	});

	test('Infers from opus.Decoder', () => {
		const resource = createAudioResource(new opus.Decoder({ rate: 48_000, channels: 2, frameSize: 960 }));
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Raw, NO_CONSTRAINT);
		expect(resource.volume).toBeUndefined();
	});

	test('Infers from VolumeTransformer', () => {
		// Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer
		const stream = new VolumeTransformer({ type: 's16le' } as any);
		const resource = createAudioResource(stream, { inlineVolume: true });
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Raw, NO_CONSTRAINT);
		expect(resource.volume).toEqual(stream);
	});

	test('Falls back to Arbitrary for unknown stream type', () => {
		const resource = createAudioResource(new PassThrough());
		expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, NO_CONSTRAINT);
		expect(resource.volume).toBeUndefined();
	});

	test('Appends silence frames when ended', async () => {
		const stream = Readable.from(Buffer.from([1]));

		const resource = new AudioResource([], [stream], null, 5);

		await started(resource);
		expect(resource.readable).toEqual(true);
		expect(resource.read()).toEqual(Buffer.from([1]));
		for (let index = 0; index < 5; index++) {
			await wait();
			expect(resource.readable).toEqual(true);
			expect(resource.read()).toEqual(SILENCE_FRAME);
		}

		await wait();
		expect(resource.readable).toEqual(false);
		expect(resource.read()).toEqual(null);
	});
});
