import { expect } from '@jest/globals';
import { container } from '../../src/config/dependency-injector.config';
import INTERNAL_CONFIG from '../../src/config/internal-config';
import { TokenService } from '../../src/services';
import {
	MeetingEndAction,
	MeetRecordingAccess,
	MeetRecordingInfo,
	MeetRecordingStatus,
	MeetRoom,
	MeetRoomAutoDeletionPolicy,
	MeetRoomDeletionPolicyWithMeeting,
	MeetRoomDeletionPolicyWithRecordings,
	MeetRoomPreferences,
	MeetRoomStatus,
	ParticipantPermissions,
	ParticipantRole
} from '../../src/typings/ce';

export const expectErrorResponse = (
	response: any,
	status = 422,
	error = 'Unprocessable Entity',
	message = 'Invalid request',
	details?: Array<{ field?: string; message: string }>
) => {
	expect(response.status).toBe(status);
	expect(response.body).toMatchObject({
		...(error ? { error } : {}),
		...(message ? { message } : {})
	});

	if (details === undefined) {
		expect(response.body.details).toBeUndefined();
		return;
	}

	expect(Array.isArray(response.body.details)).toBe(true);
	expect(response.body.details).toEqual(
		expect.arrayContaining(
			details.map((d) => {
				const matcher: any = { message: expect.stringContaining(d.message) };

				if (d.field !== undefined) {
					matcher.field = d.field;
				}

				return expect.objectContaining(matcher);
			})
		)
	);
};

export const expectValidationError = (response: any, field: string, message: string) => {
	expectErrorResponse(response, 422, 'Unprocessable Entity', 'Invalid request', [{ field, message }]);
};

/**
 * Asserts that a rooms response matches the expected values for testing purposes.
 * Validates the room array length and pagination properties.
 *
 * @param body - The API response body to validate
 * @param expectedRoomLength - The expected number of rooms in the response
 * @param expectedMaxItems - The expected maximum number of items in pagination
 * @param expectedTruncated - The expected value for pagination.isTruncated flag
 * @param expectedNextPageToken - The expected presence of pagination.nextPageToken
 *                               (if true, expects nextPageToken to be defined;
 *                                if false, expects nextPageToken to be undefined)
 */
export const expectSuccessRoomsResponse = (
	response: any,
	expectedRoomLength: number,
	expectedMaxItems: number,
	expectedTruncated: boolean,
	expectedNextPageToken: boolean
) => {
	const { body } = response;
	expect(response.status).toBe(200);
	expect(body).toBeDefined();
	expect(body.rooms).toBeDefined();
	expect(Array.isArray(body.rooms)).toBe(true);
	expect(body.rooms.length).toBe(expectedRoomLength);
	expect(body.pagination).toBeDefined();
	expect(body.pagination.isTruncated).toBe(expectedTruncated);

	expectedNextPageToken
		? expect(body.pagination.nextPageToken).toBeDefined()
		: expect(body.pagination.nextPageToken).toBeUndefined();
	expect(body.pagination.maxItems).toBe(expectedMaxItems);
};

export const expectSuccessRoomResponse = (
	response: any,
	roomName: string,
	autoDeletionDate?: number,
	preferences?: MeetRoomPreferences
) => {
	expect(response.status).toBe(200);
	expectValidRoom(response.body, roomName, preferences, autoDeletionDate);
};

export const expectSuccessRoomPreferencesResponse = (response: any, preferences: MeetRoomPreferences) => {
	expect(response.status).toBe(200);
	expect(response.body).toBeDefined();
	expect(response.body).toEqual(preferences);
};

export const expectValidRoom = (
	room: MeetRoom,
	name: string,
	preferences?: MeetRoomPreferences,
	autoDeletionDate?: number,
	autoDeletionPolicy?: MeetRoomAutoDeletionPolicy,
	status?: MeetRoomStatus,
	meetingEndAction?: MeetingEndAction
) => {
	expect(room).toBeDefined();

	expect(room.roomId).toBeDefined();
	expect(room.roomName).toBeDefined();
	expect(room.roomName).toBe(name);
	expect(room.roomId).not.toBe('');
	expect(room.roomId).toContain(room.roomName.replace(/\s+/g, '')); // Ensure roomId contains the name without spaces
	expect(room.creationDate).toBeDefined();

	if (autoDeletionDate !== undefined) {
		expect(room.autoDeletionDate).toBeDefined();
		expect(room.autoDeletionDate).toBe(autoDeletionDate);
	} else {
		expect(room.autoDeletionDate).toBeUndefined();
	}

	if (autoDeletionPolicy !== undefined) {
		expect(room.autoDeletionPolicy).toBeDefined();
		expect(room.autoDeletionPolicy).toEqual(autoDeletionPolicy);
	} else {
		expect(room.autoDeletionPolicy).toEqual({
			withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS,
			withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE
		});
	}

	expect(room.preferences).toBeDefined();

	if (preferences !== undefined) {
		expect(room.preferences).toEqual(preferences);
	} else {
		expect(room.preferences).toEqual({
			recordingPreferences: {
				enabled: true,
				allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
			},
			chatPreferences: { enabled: true },
			virtualBackgroundPreferences: { enabled: true }
		});
	}

	expect(room.moderatorUrl).toBeDefined();
	expect(room.speakerUrl).toBeDefined();
	expect(room.moderatorUrl).toContain(room.roomId);
	expect(room.speakerUrl).toContain(room.roomId);

	expect(room.status).toBeDefined();
	expect(room.status).toEqual(status || MeetRoomStatus.OPEN);
	expect(room.meetingEndAction).toBeDefined();
	expect(room.meetingEndAction).toEqual(meetingEndAction || MeetingEndAction.NONE);
};

export const expectValidRecording = (
	recording: MeetRecordingInfo,
	recordingId: string,
	roomId: string,
	roomName: string,
	status: MeetRecordingStatus
) => {
	expect(recording).toBeDefined();
	expect(recording.recordingId).toBeDefined();
	expect(recording.roomId).toBeDefined();
	expect(recording.roomName).toBeDefined();
	expect(recording.recordingId).toBe(recordingId);
	expect(recording.roomId).toBe(roomId);
	expect(recording.roomName).toBe(roomName);
	expect(recording.startDate).toBeDefined();
	expect(recording.status).toBeDefined();
	expect(recording.status).toBe(status);
	expect(recording.filename).toBeDefined();
	expect(recording.details).toBeDefined();
};

export const expectValidRoomWithFields = (room: MeetRoom, fields: string[] = []) => {
	expect(room).toBeDefined();
	expectObjectFields(room, fields);
};

export const expectValidRecordingWithFields = (rec: MeetRecordingInfo, fields: string[] = []) => {
	expect(rec).toBeDefined();
	expectObjectFields(rec, fields);
};

const expectObjectFields = (obj: any, present: string[] = [], absent: string[] = []) => {
	present.forEach((key) => {
		expect(obj).toHaveProperty(key);
		expect((obj as any)[key]).not.toBeUndefined();
	});
	absent.forEach((key) => {
		// Si la propiedad existe, debe ser undefined
		expect(Object.prototype.hasOwnProperty.call(obj, key) ? (obj as any)[key] : undefined).toBeUndefined();
	});
};

// Validate recording location header in the response
export const expectValidRecordingLocationHeader = (response: any) => {
	const locationHeader = response.headers.location;
	expect(locationHeader).toBeDefined();
	const locationHeaderUrl = new URL(locationHeader);
	expect(locationHeaderUrl.pathname).toBe(
		`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${response.body.recordingId}`
	);
};

/**
 * Validates a successful recording media response, supporting edge cases and range requests.
 *
 * @param response - The HTTP response object to validate
 * @param range - Optional range header that was sent in the request
 * @param fullSize - Optional total file size for range validation
 * @param options - Optional configuration to handle edge cases:
 *   - allowSizeDifference: Allows a difference between content-length and actual body size (default: false)
 *   - ignoreRangeFormat: Ignores exact range format checking (useful for adjusted ranges) (default: false)
 *   - expectedStatus: Override the expected status code (default: auto-determined based on range)
 */
export const expectSuccessRecordingMediaResponse = (
	response: any,
	range?: string,
	fullSize?: number,
	options?: {
		allowSizeDifference?: boolean;
		ignoreRangeFormat?: boolean;
		expectedStatus?: number;
	}
) => {
	// Default options
	const opts = {
		allowSizeDifference: false,
		ignoreRangeFormat: false,
		...options
	};

	// Determine expected status
	const expectedStatus = opts.expectedStatus ?? (range ? 206 : 200);

	// Basic validations for any successful response
	expect(response.status).toBe(expectedStatus);
	expect(response.headers['content-type']).toBe('video/mp4');
	expect(response.headers['accept-ranges']).toBe('bytes');
	expect(response.headers['content-length']).toBeDefined();
	expect(parseInt(response.headers['content-length'])).toBeGreaterThan(0);
	expect(response.headers['cache-control']).toBeDefined();

	// Verify response is binary data with some size
	expect(response.body).toBeInstanceOf(Buffer);
	expect(response.body.length).toBeGreaterThan(0);

	// Handle range responses (206 Partial Content)
	if (range && expectedStatus === 206) {
		// Verify the content-range header
		expect(response.headers['content-range']).toBeDefined();

		// If ignoreRangeFormat is true, only check the format of the content-range header
		if (opts.ignoreRangeFormat) {
			expect(response.headers['content-range']).toMatch(/^bytes \d+-\d+\/\d+$/);

			if (fullSize) {
				// Verify the total size in content-range header
				const totalSizeMatch = response.headers['content-range'].match(/\/(\d+)$/);

				if (totalSizeMatch) {
					expect(parseInt(totalSizeMatch[1])).toBe(fullSize);
				}
			}
		} else {
			// Extract the requested range from the request header
			const rangeMatch = range.match(/^bytes=(\d+)-(\d*)$/);

			if (!rangeMatch) {
				throw new Error(`Invalid range format: ${range}`);
			}

			const requestedStart = parseInt(rangeMatch[1]);
			const requestedEnd = rangeMatch[2] ? parseInt(rangeMatch[2]) : fullSize ? fullSize - 1 : undefined;

			expect(requestedStart).not.toBeNaN();

			// Verify the range in the response
			const contentRangeMatch = response.headers['content-range'].match(/^bytes (\d+)-(\d+)\/(\d+)$/);

			if (!contentRangeMatch) {
				throw new Error(`Invalid content-range format: ${response.headers['content-range']}`);
			}

			const actualStart = parseInt(contentRangeMatch[1]);
			const actualEnd = parseInt(contentRangeMatch[2]);
			const actualTotal = parseInt(contentRangeMatch[3]);

			// Verify the start matches
			expect(actualStart).toBe(requestedStart);

			// If full size is provided, verify the total is correct
			if (fullSize) {
				expect(actualTotal).toBe(fullSize);

				// The end may be adjusted if it exceeds the total size
				if (requestedEnd !== undefined && requestedEnd >= fullSize) {
					expect(actualEnd).toBe(fullSize - 1);
				} else if (requestedEnd !== undefined) {
					expect(actualEnd).toBe(requestedEnd);
				}
			}
		}

		// Verify that Content-Length is consistent
		const declaredLength = parseInt(response.headers['content-length']);
		expect(declaredLength).toBeGreaterThan(0);

		// If size differences are not allowed, body length must match exactly
		if (!opts.allowSizeDifference) {
			expect(response.body.length).toBe(declaredLength);
		} else {
			// Allow some difference but ensure it's within a reasonable tolerance
			const bodyLength = response.body.length;
			const diff = Math.abs(bodyLength - declaredLength);
			const tolerance = Math.max(declaredLength * 0.05, 10); // 5% or at least 10 bytes

			expect(diff).toBeLessThanOrEqual(tolerance);
		}
	} else if (expectedStatus === 200) {
		// For full content responses
		const declaredLength = parseInt(response.headers['content-length']);

		if (!opts.allowSizeDifference) {
			expect(response.body.length).toBe(declaredLength);
		}

		// If full size is provided, content-length must match
		if (fullSize !== undefined) {
			expect(declaredLength).toBe(fullSize);
		}
	}
};

export const expectValidStartRecordingResponse = (response: any, roomId: string, roomName: string) => {
	expect(response.status).toBe(201);
	expect(response.body).toHaveProperty('recordingId');

	expectValidRecordingLocationHeader(response);

	const recordingId = response.body.recordingId;
	expect(recordingId).toBeDefined();

	expect(recordingId).toContain(roomId);
	expect(response.body).toHaveProperty('roomId', roomId);
	expect(response.body).toHaveProperty('roomName', roomName);
	expect(response.body).toHaveProperty('startDate');
	expect(response.body).toHaveProperty('status', 'ACTIVE');
	expect(response.body).toHaveProperty('filename');
	expect(response.body).not.toHaveProperty('duration');
	expect(response.body).not.toHaveProperty('endDate');
	expect(response.body).not.toHaveProperty('size');
};

export const expectValidStopRecordingResponse = (
	response: any,
	recordingId: string,
	roomId: string,
	roomName: string
) => {
	expect(response.status).toBe(202);
	expect(response.body).toBeDefined();
	expect(response.body).toHaveProperty('recordingId', recordingId);
	expect([MeetRecordingStatus.COMPLETE, MeetRecordingStatus.ENDING]).toContain(response.body.status);
	expect(response.body).toHaveProperty('roomId', roomId);
	expect(response.body).toHaveProperty('roomName', roomName);
	expect(response.body).toHaveProperty('filename');
	expect(response.body).toHaveProperty('startDate');
	expect(response.body).toHaveProperty('duration', expect.any(Number));

	expectValidRecordingLocationHeader(response);
};

export const expectValidGetRecordingResponse = (
	response: any,
	recordingId: string,
	roomId: string,
	roomName: string,
	status?: MeetRecordingStatus,
	maxSecDuration?: number
) => {
	expect(response.status).toBe(200);
	expect(response.body).toBeDefined();
	const body = response.body;

	expect(body).toMatchObject({ recordingId, roomId, roomName });

	const isRecFinished =
		status &&
		(status === MeetRecordingStatus.COMPLETE ||
			status === MeetRecordingStatus.ABORTED ||
			status === MeetRecordingStatus.FAILED ||
			status === MeetRecordingStatus.LIMIT_REACHED);
	expect(body).toEqual(
		expect.objectContaining({
			recordingId: expect.stringMatching(new RegExp(`^${recordingId}$`)),
			roomId: expect.stringMatching(new RegExp(`^${roomId}$`)),
			roomName: expect.stringMatching(new RegExp(`^${roomName}$`)),
			...(isRecFinished ? { status: expect.any(String) } : {}),
			...(isRecFinished ? { duration: expect.any(Number) } : {}),
			...(isRecFinished ? { startDate: expect.any(Number) } : {}),
			...(isRecFinished ? { endDate: expect.any(Number) } : {}),
			...(isRecFinished ? { size: expect.any(Number) } : {}),
			filename: expect.any(String),
			...(isRecFinished ? { details: expect.any(String) } : {})
		})
	);

	expect(body.status).toBeDefined();

	if (status !== undefined) {
		expect(body.status).toBe(status);
	}

	if (isRecFinished) {
		expect(body.endDate).toBeGreaterThanOrEqual(body.startDate);
		expect(body.duration).toBeGreaterThanOrEqual(0);
	}

	if (isRecFinished && maxSecDuration) {
		expect(body.duration).toBeLessThanOrEqual(maxSecDuration);

		const computedSec = (body.endDate - body.startDate) / 1000;
		const diffSec = Math.abs(maxSecDuration - computedSec);
		// Estimate 5 seconds of tolerace because of time to start/stop recording
		expect(diffSec).toBeLessThanOrEqual(5);
	}
};

export const expectSuccessListRecordingResponse = (
	response: any,
	recordingLength: number,
	isTruncated: boolean,
	nextPageToken: boolean,
	maxItems = 10
) => {
	expect(response.status).toBe(200);
	expect(response.body).toBeDefined();
	expect(response.body.recordings).toBeDefined();
	expect(Array.isArray(response.body.recordings)).toBe(true);
	expect(response.body.recordings.length).toBe(recordingLength);
	expect(response.body.pagination).toBeDefined();
	expect(response.body.pagination.isTruncated).toBe(isTruncated);

	if (nextPageToken) {
		expect(response.body.pagination.nextPageToken).toBeDefined();
	} else {
		expect(response.body.pagination.nextPageToken).toBeUndefined();
	}

	expect(response.body.pagination.maxItems).toBeDefined();
	expect(response.body.pagination.maxItems).toBeGreaterThan(0);
	expect(response.body.pagination.maxItems).toBeLessThanOrEqual(100);
	expect(response.body.pagination.maxItems).toBe(maxItems);
};

export const expectValidGetRecordingUrlResponse = (response: any, recordingId: string) => {
	expect(response.status).toBe(200);
	const recordingUrl = response.body.url;
	expect(recordingUrl).toBeDefined();

	const parsedUrl = new URL(recordingUrl);
	expect(parsedUrl.pathname).toBe(`/recording/${recordingId}`);
	expect(parsedUrl.searchParams.get('secret')).toBeDefined();
};

export const expectValidRoomRolesAndPermissionsResponse = (response: any, roomId: string) => {
	expect(response.status).toBe(200);
	expect(response.body).toEqual(
		expect.arrayContaining([
			{
				role: ParticipantRole.MODERATOR,
				permissions: getPermissions(roomId, ParticipantRole.MODERATOR)
			},
			{
				role: ParticipantRole.SPEAKER,
				permissions: getPermissions(roomId, ParticipantRole.SPEAKER)
			}
		])
	);
};

export const expectValidRoomRoleAndPermissionsResponse = (
	response: any,
	roomId: string,
	participantRole: ParticipantRole
) => {
	expect(response.status).toBe(200);
	expect(response.body).toEqual({
		role: participantRole,
		permissions: getPermissions(roomId, participantRole)
	});
};

export const getPermissions = (
	roomId: string,
	role: ParticipantRole,
	addJoinPermission = true
): ParticipantPermissions => {
	switch (role) {
		case ParticipantRole.MODERATOR:
			return {
				livekit: {
					roomJoin: addJoinPermission,
					room: roomId,
					canPublish: true,
					canSubscribe: true,
					canPublishData: true,
					canUpdateOwnMetadata: true
				},
				openvidu: {
					canRecord: true,
					canChat: true,
					canChangeVirtualBackground: true
				}
			};
		case ParticipantRole.SPEAKER:
			return {
				livekit: {
					roomJoin: addJoinPermission,
					room: roomId,
					canPublish: true,
					canSubscribe: true,
					canPublishData: true,
					canUpdateOwnMetadata: true
				},
				openvidu: {
					canRecord: false,
					canChat: true,
					canChangeVirtualBackground: true
				}
			};
		default:
			throw new Error(`Unknown role ${role}`);
	}
};

export const expectValidParticipantTokenResponse = (
	response: any,
	roomId: string,
	participantRole: ParticipantRole,
	participantName?: string,
	participantIdentity?: string,
	otherRoles: ParticipantRole[] = []
) => {
	expect(response.status).toBe(200);
	expect(response.body).toHaveProperty('token');

	const token = response.body.token;
	const decodedToken = decodeJWTToken(token);

	const permissions = getPermissions(roomId, participantRole, !!participantName);
	const rolesAndPermissions = otherRoles.map((role) => ({
		role,
		permissions: getPermissions(roomId, role, !!participantName).openvidu
	}));

	if (!rolesAndPermissions.some((r) => r.role === participantRole)) {
		rolesAndPermissions.push({
			role: participantRole,
			permissions: permissions.openvidu
		});
	}

	if (participantName) {
		expect(decodedToken).toHaveProperty('name', participantName);
		expect(decodedToken).toHaveProperty('sub');

		if (participantIdentity) {
			expect(decodedToken.sub).toBe(participantIdentity);
		} else {
			expect(decodedToken.sub).toContain(participantName.replace(/\s+/g, '')); // Ensure sub contains the name without spaces
		}
	} else {
		expect(decodedToken).not.toHaveProperty('name');
		expect(decodedToken).not.toHaveProperty('sub');
	}

	expect(decodedToken).toHaveProperty('video', permissions.livekit);
	expect(decodedToken).toHaveProperty('metadata');
	const metadata = JSON.parse(decodedToken.metadata || '{}');
	expect(metadata).toHaveProperty('roles');
	expect(metadata.roles).toEqual(expect.arrayContaining(rolesAndPermissions));
	expect(metadata).toHaveProperty('selectedRole', participantRole);

	// Check that the token is included in a cookie
	expect(response.headers['set-cookie']).toBeDefined();
	const cookies = response.headers['set-cookie'] as unknown as string[];
	const participantTokenCookie = cookies.find((cookie) =>
		cookie.startsWith(`${INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME}=`)
	) as string;
	expect(participantTokenCookie).toBeDefined();
	expect(participantTokenCookie).toContain(token);
	expect(participantTokenCookie).toContain('HttpOnly');
	expect(participantTokenCookie).toContain('SameSite=Strict');
	expect(participantTokenCookie).toContain('Path=/');
};

export const expectValidRecordingTokenResponse = (
	response: any,
	roomId: string,
	participantRole: ParticipantRole,
	canRetrieveRecordings: boolean,
	canDeleteRecordings: boolean
) => {
	expect(response.status).toBe(200);
	expect(response.body).toHaveProperty('token');

	const token = response.body.token;
	const decodedToken = decodeJWTToken(token);

	expect(decodedToken).toHaveProperty('video', {
		room: roomId
	});
	expect(decodedToken).toHaveProperty('metadata');
	const metadata = JSON.parse(decodedToken.metadata || '{}');
	expect(metadata).toHaveProperty('role', participantRole);
	expect(metadata).toHaveProperty('recordingPermissions', {
		canRetrieveRecordings,
		canDeleteRecordings
	});

	// Check that the token is included in a cookie
	expect(response.headers['set-cookie']).toBeDefined();
	const cookies = response.headers['set-cookie'] as unknown as string[];
	const participantTokenCookie = cookies.find((cookie) =>
		cookie.startsWith(`${INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME}=`)
	) as string;
	expect(participantTokenCookie).toBeDefined();
	expect(participantTokenCookie).toContain(token);
	expect(participantTokenCookie).toContain('HttpOnly');
	expect(participantTokenCookie).toContain('SameSite=Strict');
	expect(participantTokenCookie).toContain('Path=/');
};

const decodeJWTToken = (token: string) => {
	const tokenService = container.get(TokenService);
	return tokenService.getClaimsIgnoringExpiration(token);
};
