/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/dot-notation */
import { EventEmitter } from 'node:events';
import type { Mocked } from 'vitest';
import { vitest, describe, test, expect, beforeEach } from 'vitest';
import * as _DataStore from '../src/DataStore';
import {
	createVoiceConnection,
	VoiceConnection,
	VoiceConnectionDisconnectReason,
	VoiceConnectionStatus,
	type VoiceConnectionConnectingState,
	type VoiceConnectionReadyState,
	type VoiceConnectionSignallingState,
} from '../src/VoiceConnection';
import * as _AudioPlayer from '../src/audio/AudioPlayer';
import { PlayerSubscription as _PlayerSubscription } from '../src/audio/PlayerSubscription';
import * as Networking from '../src/networking/Networking';
import type { DiscordGatewayAdapterLibraryMethods } from '../src/util/adapter';

vitest.mock('../src/audio/AudioPlayer');
vitest.mock('../src/audio/PlayerSubscription');
vitest.mock('../src/DataStore');
vitest.mock('../src/networking/Networking', async (importOriginal) => {
	// eslint-disable-next-line @typescript-eslint/consistent-type-imports
	const actual = await importOriginal<typeof import('../src/networking/Networking')>();
	const Networking = actual.Networking;
	Networking.prototype['createWebSocket'] = vitest.fn();
	return {
		...actual,
		Networking,
	};
});

const DataStore = _DataStore as unknown as Mocked<typeof _DataStore>;
const AudioPlayer = _AudioPlayer as unknown as Mocked<typeof _AudioPlayer>;
const PlayerSubscription = _PlayerSubscription as unknown as Mocked<typeof _PlayerSubscription>;

const _NetworkingClass = Networking.Networking;
// eslint-disable-next-line prefer-arrow-callback
vitest.spyOn(Networking, 'Networking').mockImplementation(function Networking(...args) {
	return new _NetworkingClass(...args);
});

function createFakeAdapter() {
	const sendPayload = vitest.fn();
	sendPayload.mockReturnValue(true);
	const destroy = vitest.fn();
	const libMethods: Partial<DiscordGatewayAdapterLibraryMethods> = {};
	return {
		sendPayload,
		destroy,
		libMethods,
		creator: vitest.fn((methods) => {
			Object.assign(libMethods, methods);
			return {
				sendPayload,
				destroy,
			};
		}),
	};
}

function createJoinConfig() {
	return {
		channelId: '1',
		guildId: '2',
		selfDeaf: true,
		selfMute: false,
		group: 'default',
	};
}

function createFakeVoiceConnection() {
	const adapter = createFakeAdapter();
	const joinConfig = createJoinConfig();
	const voiceConnection = new VoiceConnection(joinConfig, {
		debug: false,
		adapterCreator: adapter.creator,
	});
	return { adapter, joinConfig, voiceConnection };
}

beforeEach(() => {
	DataStore.createJoinVoiceChannelPayload.mockReset();
	DataStore.getVoiceConnection.mockReset();
	DataStore.trackVoiceConnection.mockReset();
	DataStore.untrackVoiceConnection.mockReset();
});

describe('createVoiceConnection', () => {
	test('New voice connection', () => {
		const mockPayload = Symbol('mock') as any;
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => mockPayload);
		const adapter = createFakeAdapter();
		const joinConfig = createJoinConfig();
		const voiceConnection = createVoiceConnection(joinConfig, {
			debug: false,
			adapterCreator: adapter.creator,
		});
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
		expect(DataStore.getVoiceConnection).toHaveBeenCalledTimes(1);
		expect(DataStore.trackVoiceConnection).toHaveBeenCalledWith(voiceConnection);
		expect(DataStore.untrackVoiceConnection).not.toHaveBeenCalled();
		expect(adapter.sendPayload).toHaveBeenCalledWith(mockPayload);
	});

	test('New voice connection with adapter failure', () => {
		const mockPayload = Symbol('mock') as any;
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => mockPayload);
		const adapter = createFakeAdapter();
		adapter.sendPayload.mockReturnValue(false);
		const joinConfig = createJoinConfig();
		const voiceConnection = createVoiceConnection(joinConfig, {
			debug: false,
			adapterCreator: adapter.creator,
		});
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected);
		expect(DataStore.getVoiceConnection).toHaveBeenCalledTimes(1);
		expect(DataStore.trackVoiceConnection).toHaveBeenCalledWith(voiceConnection);
		expect(DataStore.untrackVoiceConnection).not.toHaveBeenCalled();
		expect(adapter.sendPayload).toHaveBeenCalledWith(mockPayload);
	});

	test('Reconfiguring existing connection', () => {
		const mockPayload = Symbol('mock') as any;

		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => mockPayload);

		const existingAdapter = createFakeAdapter();
		const existingJoinConfig = createJoinConfig();
		const existingVoiceConnection = new VoiceConnection(existingJoinConfig, {
			debug: false,
			adapterCreator: existingAdapter.creator,
		});

		const stateSetter = vitest.spyOn(existingVoiceConnection, 'state', 'set');

		DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
			guildId === existingJoinConfig.guildId && group === existingJoinConfig.group
				? existingVoiceConnection
				: undefined,
		);

		const newAdapter = createFakeAdapter();
		const newJoinConfig = createJoinConfig();
		const newVoiceConnection = createVoiceConnection(newJoinConfig, {
			debug: false,
			adapterCreator: newAdapter.creator,
		});
		expect(DataStore.getVoiceConnection).toHaveBeenCalledWith(newJoinConfig.guildId, newJoinConfig.group);
		expect(DataStore.trackVoiceConnection).not.toHaveBeenCalled();
		expect(DataStore.untrackVoiceConnection).not.toHaveBeenCalled();
		expect(newAdapter.creator).not.toHaveBeenCalled();
		expect(existingAdapter.sendPayload).toHaveBeenCalledWith(mockPayload);
		expect(newVoiceConnection).toEqual(existingVoiceConnection);
		expect(stateSetter).not.toHaveBeenCalled();
	});

	test('Calls rejoin() on existing disconnected connection', () => {
		const mockPayload = Symbol('mock') as any;

		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => mockPayload);

		const existingAdapter = createFakeAdapter();
		const existingJoinConfig = createJoinConfig();
		const existingVoiceConnection = new VoiceConnection(existingJoinConfig, {
			debug: false,
			adapterCreator: existingAdapter.creator,
		});
		existingVoiceConnection.state = {
			status: VoiceConnectionStatus.Disconnected,
			adapter: existingAdapter,
			reason: VoiceConnectionDisconnectReason.EndpointRemoved,
		};

		const rejoinSpy = vitest.spyOn(existingVoiceConnection, 'rejoin');

		DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
			guildId === existingJoinConfig.guildId && group === existingJoinConfig.group
				? existingVoiceConnection
				: undefined,
		);

		const newAdapter = createFakeAdapter();
		const newJoinConfig = createJoinConfig();
		const { guildId, group, ...rejoinConfig } = newJoinConfig;
		const newVoiceConnection = createVoiceConnection(newJoinConfig, {
			debug: false,
			adapterCreator: newAdapter.creator,
		});
		expect(DataStore.getVoiceConnection).toHaveBeenCalledWith(newJoinConfig.guildId, newJoinConfig.group);
		expect(DataStore.trackVoiceConnection).not.toHaveBeenCalled();
		expect(DataStore.untrackVoiceConnection).not.toHaveBeenCalled();
		expect(newAdapter.creator).not.toHaveBeenCalled();
		expect(rejoinSpy).toHaveBeenCalledWith(rejoinConfig);
		expect(newVoiceConnection).toEqual(existingVoiceConnection);
	});

	test('Reconfiguring existing connection with adapter failure', () => {
		const mockPayload = Symbol('mock') as any;

		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => mockPayload);

		const existingAdapter = createFakeAdapter();
		const existingJoinConfig = createJoinConfig();
		const existingVoiceConnection = new VoiceConnection(existingJoinConfig, {
			debug: false,
			adapterCreator: existingAdapter.creator,
		});

		DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
			guildId === existingJoinConfig.guildId && group === existingJoinConfig.group
				? existingVoiceConnection
				: undefined,
		);

		const newAdapter = createFakeAdapter();
		const newJoinConfig = createJoinConfig();
		existingAdapter.sendPayload.mockReturnValue(false);
		const newVoiceConnection = createVoiceConnection(newJoinConfig, {
			debug: false,
			adapterCreator: newAdapter.creator,
		});
		expect(DataStore.getVoiceConnection).toHaveBeenCalledWith(newJoinConfig.guildId, newJoinConfig.group);
		expect(DataStore.trackVoiceConnection).not.toHaveBeenCalled();
		expect(DataStore.untrackVoiceConnection).not.toHaveBeenCalled();
		expect(newAdapter.creator).not.toHaveBeenCalled();
		expect(existingAdapter.sendPayload).toHaveBeenCalledWith(mockPayload);
		expect(newVoiceConnection).toEqual(existingVoiceConnection);
		expect(newVoiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected);
	});
});

describe('VoiceConnection#addServerPacket', () => {
	test('Stores the packet and attempts to configure networking', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		voiceConnection.configureNetworking = vitest.fn();
		const dummy = {
			endpoint: 'discord.com',
			guild_id: 123,
			token: 'abc',
		} as any;
		voiceConnection['addServerPacket'](dummy);
		expect(voiceConnection['packets'].server).toEqual(dummy);
		expect(voiceConnection.configureNetworking).toHaveBeenCalled();
	});

	test('Overwrites existing packet', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		voiceConnection['packets'].server = Symbol('old') as any;
		voiceConnection.configureNetworking = vitest.fn();
		const dummy = {
			endpoint: 'discord.com',
			guild_id: 123,
			token: 'abc',
		} as any;
		voiceConnection['addServerPacket'](dummy);
		expect(voiceConnection['packets'].server).toEqual(dummy);
		expect(voiceConnection.configureNetworking).toHaveBeenCalled();
	});

	test('Disconnects when given a null endpoint', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		voiceConnection['packets'].server = Symbol('old') as any;
		voiceConnection.configureNetworking = vitest.fn();
		const dummy = {
			endpoint: null,
			guild_id: 123,
			token: 'abc',
		} as any;
		voiceConnection['addServerPacket'](dummy);
		expect(voiceConnection['packets'].server).toEqual(dummy);
		expect(voiceConnection.configureNetworking).not.toHaveBeenCalled();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected);
	});
});

describe('VoiceConnection#addStatePacket', () => {
	test('State is assigned to joinConfig', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		voiceConnection['addStatePacket']({
			self_deaf: true,
			self_mute: true,
			channel_id: '123',
		} as any);

		expect(voiceConnection.joinConfig).toMatchObject({
			selfDeaf: true,
			selfMute: true,
			channelId: '123',
		});

		voiceConnection['addStatePacket']({
			self_mute: false,
		} as any);

		expect(voiceConnection.joinConfig).toMatchObject({
			selfDeaf: true,
			selfMute: false,
			channelId: '123',
		});
	});
});

describe('VoiceConnection#configureNetworking', () => {
	test('Only creates Networking instance when both packets are present and not destroyed', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);

		voiceConnection.configureNetworking();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
		const adapter = (voiceConnection.state as VoiceConnectionSignallingState).adapter;

		const state = {
			session_id: 'abc',
			user_id: '123',
		} as any;

		const server = {
			endpoint: 'def',
			guild_id: '123',
			token: 'xyz',
		} as any;

		Object.assign(voiceConnection['packets'], { state, server: undefined });
		voiceConnection.configureNetworking();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
		expect(Networking.Networking).toHaveBeenCalledTimes(0);

		Object.assign(voiceConnection['packets'], { state: undefined, server });
		voiceConnection.configureNetworking();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
		expect(Networking.Networking).toHaveBeenCalledTimes(0);

		Object.assign(voiceConnection['packets'], { state, server });
		voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
		voiceConnection.configureNetworking();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
		expect(Networking.Networking).toHaveBeenCalledTimes(0);

		voiceConnection.state = { status: VoiceConnectionStatus.Signalling, adapter };
		voiceConnection.configureNetworking();
		expect(Networking.Networking).toHaveBeenCalledTimes(1);
		expect(Networking.Networking).toHaveBeenCalledWith(
			{
				endpoint: server.endpoint,
				serverId: server.guild_id,
				token: server.token,
				sessionId: state.session_id,
				userId: state.user_id,
			},
			{
				daveEncryption: true,
				debug: false,
				decryptionFailureTolerance: undefined,
			},
		);
		expect(voiceConnection.state).toMatchObject({
			status: VoiceConnectionStatus.Connecting,
			adapter,
		});
		expect((voiceConnection.state as unknown as VoiceConnectionConnectingState).networking).toBeInstanceOf(
			_NetworkingClass,
		);
	});
});

describe('VoiceConnection#onNetworkingClose', () => {
	test('Does nothing in destroyed state', () => {
		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = {
			status: VoiceConnectionStatus.Destroyed,
		};
		voiceConnection['onNetworkingClose'](1_000);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
		expect(adapter.sendPayload).not.toHaveBeenCalled();
	});

	test('Disconnects for code 4014', () => {
		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection['onNetworkingClose'](4_014);
		expect(voiceConnection.state).toMatchObject({
			status: VoiceConnectionStatus.Disconnected,
			closeCode: 4_014,
		});
		expect(adapter.sendPayload).not.toHaveBeenCalled();
	});

	test('Attempts rejoin for codes != 4014', () => {
		const dummyPayload = Symbol('dummy') as any;
		const { voiceConnection, adapter, joinConfig } = createFakeVoiceConnection();
		DataStore.createJoinVoiceChannelPayload.mockImplementation((config) =>
			config === joinConfig ? dummyPayload : undefined,
		);
		voiceConnection['onNetworkingClose'](1_234);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
		expect(adapter.sendPayload).toHaveBeenCalledWith(dummyPayload);
		expect(voiceConnection.rejoinAttempts).toEqual(1);
	});

	test('Attempts rejoin for codes != 4014 (with adapter failure)', () => {
		const dummyPayload = Symbol('dummy') as any;
		const { voiceConnection, adapter, joinConfig } = createFakeVoiceConnection();
		DataStore.createJoinVoiceChannelPayload.mockImplementation((config) =>
			config === joinConfig ? dummyPayload : undefined,
		);
		adapter.sendPayload.mockReturnValue(false);
		voiceConnection['onNetworkingClose'](1_234);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected);
		expect(adapter.sendPayload).toHaveBeenCalledWith(dummyPayload);
		expect(voiceConnection.rejoinAttempts).toEqual(1);
	});
});

describe('VoiceConnection#onNetworkingStateChange', () => {
	test('Does nothing when status code identical', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
		voiceConnection['onNetworkingStateChange'](
			{ code: Networking.NetworkingStatusCode.Ready } as any,
			{ code: Networking.NetworkingStatusCode.Ready } as any,
		);
		voiceConnection['onNetworkingStateChange'](
			{ code: Networking.NetworkingStatusCode.Closed } as any,
			{ code: Networking.NetworkingStatusCode.Closed } as any,
		);
		expect(stateSetter).not.toHaveBeenCalled();
	});

	test('Does nothing when not in Ready or Connecting states', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
		const call = [
			{ code: Networking.NetworkingStatusCode.Ready } as any,
			{ code: Networking.NetworkingStatusCode.Closed } as any,
		];
		voiceConnection['_state'] = { status: VoiceConnectionStatus.Signalling } as any;
		voiceConnection['onNetworkingStateChange'](call[0], call[1]);
		voiceConnection['_state'] = { status: VoiceConnectionStatus.Disconnected } as any;
		voiceConnection['onNetworkingStateChange'](call[0], call[1]);
		voiceConnection['_state'] = { status: VoiceConnectionStatus.Destroyed } as any;
		voiceConnection['onNetworkingStateChange'](call[0], call[1]);
		expect(stateSetter).not.toHaveBeenCalled();
	});

	test('Transitions to Ready', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
		voiceConnection['_state'] = {
			...(voiceConnection.state as VoiceConnectionSignallingState),
			status: VoiceConnectionStatus.Connecting,
			networking: new Networking.Networking({} as any, {}),
		};

		voiceConnection['onNetworkingStateChange'](
			{ code: Networking.NetworkingStatusCode.Closed } as any,
			{ code: Networking.NetworkingStatusCode.Ready } as any,
		);

		expect(stateSetter).toHaveBeenCalledTimes(1);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Ready);
	});

	test('Transitions to Connecting', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
		voiceConnection['_state'] = {
			...(voiceConnection.state as VoiceConnectionSignallingState),
			status: VoiceConnectionStatus.Connecting,
			networking: new Networking.Networking({} as any, {}),
		};

		voiceConnection['onNetworkingStateChange'](
			{ code: Networking.NetworkingStatusCode.Ready } as any,
			{ code: Networking.NetworkingStatusCode.Identifying } as any,
		);

		expect(stateSetter).toHaveBeenCalledTimes(1);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Connecting);
	});
});

describe('VoiceConnection#destroy', () => {
	test('Throws when in Destroyed state', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
		expect(() => voiceConnection.destroy()).toThrow();
	});

	test('Cleans up in a valid, destroyable state', () => {
		const { voiceConnection, joinConfig, adapter } = createFakeVoiceConnection();
		DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
			guildId === joinConfig.guildId && group === joinConfig.group ? voiceConnection : undefined,
		);
		const dummy = Symbol('dummy');
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => dummy as any);
		voiceConnection.destroy();
		expect(DataStore.getVoiceConnection).toHaveReturnedWith(voiceConnection);
		expect(DataStore.untrackVoiceConnection).toHaveBeenCalledWith(voiceConnection);
		expect(DataStore.createJoinVoiceChannelPayload.mock.calls[0]?.[0]).toMatchObject({
			channelId: null,
			guildId: joinConfig.guildId,
		});
		expect(adapter.sendPayload).toHaveBeenCalledWith(dummy);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
	});
});

describe('VoiceConnection#disconnect', () => {
	test('Fails in Destroyed and Signalling states', () => {
		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
		expect(voiceConnection.disconnect()).toEqual(false);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);

		voiceConnection.state = { status: VoiceConnectionStatus.Signalling, adapter };
		expect(voiceConnection.disconnect()).toEqual(false);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
	});

	test('Disconnects - available adapter', () => {
		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = {
			status: VoiceConnectionStatus.Ready,
			adapter,
			networking: new Networking.Networking({} as any, {}),
		};
		const leavePayload = Symbol('dummy');
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => leavePayload as any);
		expect(voiceConnection.disconnect()).toEqual(true);
		expect(voiceConnection.joinConfig).toMatchObject({
			channelId: null,
			guildId: '2',
			selfDeaf: true,
			selfMute: false,
		});
		expect(DataStore.createJoinVoiceChannelPayload).toHaveBeenCalledWith(voiceConnection.joinConfig);
		expect(adapter.sendPayload).toHaveBeenCalledWith(leavePayload);
		expect(voiceConnection.state).toMatchObject({
			status: VoiceConnectionStatus.Disconnected,
			reason: VoiceConnectionDisconnectReason.Manual,
		});
	});

	test('Disconnects - unavailable adapter', () => {
		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = {
			status: VoiceConnectionStatus.Ready,
			adapter,
			networking: new Networking.Networking({} as any, {}),
		};
		adapter.sendPayload.mockImplementation(() => false);
		expect(voiceConnection.disconnect()).toEqual(false);
		expect(voiceConnection.state).toMatchObject({
			status: VoiceConnectionStatus.Disconnected,
			reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
		});
	});
});

describe('VoiceConnection#rejoin', () => {
	test('Rejoins in a disconnected state', () => {
		const dummy = Symbol('dummy') as any;
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => dummy);

		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = {
			...(voiceConnection.state as VoiceConnectionSignallingState),
			status: VoiceConnectionStatus.Disconnected,
			reason: VoiceConnectionDisconnectReason.WebSocketClose,
			closeCode: 1_000,
		};
		expect(voiceConnection.rejoin()).toEqual(true);
		expect(voiceConnection.rejoinAttempts).toEqual(1);
		expect(adapter.sendPayload).toHaveBeenCalledWith(dummy);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
	});

	test('Rejoins in a ready state', () => {
		const dummy = Symbol('dummy') as any;
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => dummy);

		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = {
			...(voiceConnection.state as VoiceConnectionReadyState),
			status: VoiceConnectionStatus.Ready,
		};
		expect(voiceConnection.rejoin()).toEqual(true);
		expect(voiceConnection.rejoinAttempts).toEqual(0);
		expect(adapter.sendPayload).toHaveBeenCalledWith(dummy);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Ready);
	});

	test('Stays in the disconnected state when the adapter fails', () => {
		const dummy = Symbol('dummy') as any;
		DataStore.createJoinVoiceChannelPayload.mockImplementation(() => dummy);

		const { voiceConnection, adapter } = createFakeVoiceConnection();
		voiceConnection.state = {
			...(voiceConnection.state as VoiceConnectionSignallingState),
			status: VoiceConnectionStatus.Disconnected,
			reason: VoiceConnectionDisconnectReason.WebSocketClose,
			closeCode: 1_000,
		};
		adapter.sendPayload.mockReturnValue(false);
		expect(voiceConnection.rejoin()).toEqual(false);
		expect(voiceConnection.rejoinAttempts).toEqual(1);
		expect(adapter.sendPayload).toHaveBeenCalledWith(dummy);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected);
	});
});

describe('VoiceConnection#subscribe', () => {
	test('Does nothing in Destroyed state', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const player = new AudioPlayer.AudioPlayer();
		player['subscribe'] = vitest.fn();
		voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
		expect(voiceConnection.subscribe(player)).toBeUndefined();
		expect(player['subscribe']).not.toHaveBeenCalled();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
	});

	test('Subscribes in a live state', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const adapter = (voiceConnection.state as VoiceConnectionSignallingState).adapter;
		const player = new AudioPlayer.AudioPlayer();
		const dummy = Symbol('dummy');
		player['subscribe'] = vitest.fn().mockImplementation(() => dummy);
		expect(voiceConnection.subscribe(player)).toEqual(dummy);
		expect(player['subscribe']).toHaveBeenCalledWith(voiceConnection);
		expect(voiceConnection.state).toMatchObject({
			status: VoiceConnectionStatus.Signalling,
			adapter,
		});
	});
});

describe('VoiceConnection#onSubscriptionRemoved', () => {
	test('Does nothing in Destroyed state', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer());
		subscription.unsubscribe = vitest.fn();

		voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
		voiceConnection['onSubscriptionRemoved'](subscription);
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
		expect(subscription.unsubscribe).not.toHaveBeenCalled();
	});

	test('Does nothing when subscription is not the same as the stored one', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer());
		subscription.unsubscribe = vitest.fn();

		voiceConnection.state = { ...(voiceConnection.state as VoiceConnectionSignallingState), subscription };
		voiceConnection['onSubscriptionRemoved'](Symbol('new subscription') as any);
		expect(voiceConnection.state).toMatchObject({
			status: VoiceConnectionStatus.Signalling,
			subscription,
		});
		expect(subscription.unsubscribe).not.toHaveBeenCalled();
	});

	test('Unsubscribes in a live state with matching subscription', () => {
		const { voiceConnection } = createFakeVoiceConnection();
		const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer());
		subscription.unsubscribe = vitest.fn();

		voiceConnection.state = { ...(voiceConnection.state as VoiceConnectionSignallingState), subscription };
		voiceConnection['onSubscriptionRemoved'](subscription);
		expect(voiceConnection.state).toEqual({
			...voiceConnection.state,
			subscription: undefined,
		});
		expect(subscription.unsubscribe).toHaveBeenCalledTimes(1);
	});

	describe('updateReceiveBindings', () => {
		test('Applies and removes udp listeners', () => {
			// Arrange
			const ws = new EventEmitter() as any;

			const oldNetworking = new Networking.Networking({} as any, {});
			oldNetworking.state = {
				code: Networking.NetworkingStatusCode.Ready,
				connectionData: {} as any,
				connectionOptions: {} as any,
				udp: new EventEmitter() as any,
				ws,
			};

			const newNetworking = new Networking.Networking({} as any, {});
			newNetworking.state = {
				...oldNetworking.state,
				udp: new EventEmitter() as any,
			};

			const { voiceConnection } = createFakeVoiceConnection();

			// Act
			voiceConnection['updateReceiveBindings'](newNetworking.state, oldNetworking.state);

			// Assert
			expect(oldNetworking.state.udp.listenerCount('message')).toEqual(0);
			expect(newNetworking.state.udp.listenerCount('message')).toEqual(1);
			expect(voiceConnection.receiver.connectionData).toEqual(newNetworking.state.connectionData);
		});

		test('Applies and removes ws listeners', () => {
			// Arrange
			const udp = new EventEmitter() as any;

			const oldNetworking = new Networking.Networking({} as any, {});
			oldNetworking.state = {
				code: Networking.NetworkingStatusCode.Ready,
				connectionData: {} as any,
				connectionOptions: {} as any,
				udp,
				ws: new EventEmitter() as any,
			};

			const newNetworking = new Networking.Networking({} as any, {});
			newNetworking.state = {
				...oldNetworking.state,
				ws: new EventEmitter() as any,
			};

			const { voiceConnection } = createFakeVoiceConnection();

			// Act
			voiceConnection['updateReceiveBindings'](newNetworking.state, oldNetworking.state);

			// Assert
			expect(oldNetworking.state.ws.listenerCount('packet')).toEqual(0);
			expect(newNetworking.state.ws.listenerCount('packet')).toEqual(1);
			expect(voiceConnection.receiver.connectionData).toEqual(newNetworking.state.connectionData);
		});

		test('Applies initial listeners', () => {
			// Arrange

			const newNetworking = new Networking.Networking({} as any, {});
			newNetworking.state = {
				code: Networking.NetworkingStatusCode.Ready,
				connectionData: {} as any,
				connectionOptions: {} as any,
				udp: new EventEmitter() as any,
				ws: new EventEmitter() as any,
			};

			const { voiceConnection } = createFakeVoiceConnection();

			// Act
			voiceConnection['updateReceiveBindings'](newNetworking.state, undefined);

			// Assert
			expect(newNetworking.state.ws.listenerCount('packet')).toEqual(1);
			expect(newNetworking.state.udp.listenerCount('message')).toEqual(1);
			expect(voiceConnection.receiver.connectionData).toEqual(newNetworking.state.connectionData);
		});
	});
});

describe('Adapter', () => {
	test('onVoiceServerUpdate', () => {
		const { adapter, voiceConnection } = createFakeVoiceConnection();
		voiceConnection['addServerPacket'] = vitest.fn();
		const dummy = Symbol('dummy') as any;
		adapter.libMethods.onVoiceServerUpdate!(dummy);
		expect(voiceConnection['addServerPacket']).toHaveBeenCalledWith(dummy);
	});

	test('onVoiceStateUpdate', () => {
		const { adapter, voiceConnection } = createFakeVoiceConnection();
		voiceConnection['addStatePacket'] = vitest.fn();
		const dummy = Symbol('dummy') as any;
		adapter.libMethods.onVoiceStateUpdate!(dummy);
		expect(voiceConnection['addStatePacket']).toHaveBeenCalledWith(dummy);
	});

	test('destroy', () => {
		const { adapter, voiceConnection } = createFakeVoiceConnection();
		adapter.libMethods.destroy!();
		expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
		expect(adapter.sendPayload).not.toHaveBeenCalled();
	});
});
