import * as helpers from '../../../actions/file/helpers';
import * as GenericFunctions from '../../../GenericFunctions';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
import type { Mock } from 'vitest';

const mockFileCreateResponse = {
	data: {
		id: 'file-123',
		uploadUrl: 'https://upload.example.com/url',
	},
};

// Mock the transport and other dependencies
vi.mock('../../../transport', async () => {
	const originalModule = await vi.importActual<typeof transport>('../../../transport');
	return {
		...originalModule,
		apiRequest: vi.fn(async () => {}),
	};
});

vi.mock('../../../GenericFunctions', async () => {
	const originalModule = await vi.importActual<typeof GenericFunctions>(
		'../../../GenericFunctions',
	);
	return {
		...originalModule,
		waitForSessionEvent: vi.fn(),
	};
});

describe('Test Airtop file helpers', () => {
	afterEach(() => {
		vi.clearAllMocks();
		(transport.apiRequest as Mock).mockReset();
		(GenericFunctions.waitForSessionEvent as Mock).mockReset();
	});

	describe('requestAllFiles', () => {
		it('should request all files with pagination', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			const mockFilesResponse1 = {
				data: {
					files: [{ id: 'file-1' }, { id: 'file-2' }],
					pagination: { hasMore: true },
				},
			};

			const mockFilesResponse2 = {
				data: {
					files: [{ id: 'file-3' }],
					pagination: { hasMore: false },
				},
			};

			apiRequestMock
				.mockResolvedValueOnce(mockFilesResponse1)
				.mockResolvedValueOnce(mockFilesResponse2);

			const result = await helpers.requestAllFiles.call(
				createMockExecuteFunction({}),
				'session-123',
			);

			expect(apiRequestMock).toHaveBeenCalledTimes(2);
			expect(apiRequestMock).toHaveBeenNthCalledWith(
				1,
				'GET',
				'/files',
				{},
				{ offset: 0, limit: 100, sessionIds: 'session-123' },
			);
			expect(apiRequestMock).toHaveBeenNthCalledWith(
				2,
				'GET',
				'/files',
				{},
				{ offset: 100, limit: 100, sessionIds: 'session-123' },
			);

			expect(result).toEqual({
				data: {
					files: [{ id: 'file-1' }, { id: 'file-2' }, { id: 'file-3' }],
					pagination: { hasMore: false },
				},
			});
		});

		it('should handle empty response', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			const mockEmptyResponse = {
				data: {
					files: [],
					pagination: { hasMore: false },
				},
			};

			apiRequestMock.mockResolvedValueOnce(mockEmptyResponse);

			const result = await helpers.requestAllFiles.call(
				createMockExecuteFunction({}),
				'session-123',
			);

			expect(apiRequestMock).toHaveBeenCalledTimes(1);
			expect(result).toEqual({
				data: {
					files: [],
					pagination: { hasMore: false },
				},
			});
		});
	});

	describe('pollFileUntilAvailable', () => {
		it('should poll until file is available', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			apiRequestMock
				.mockResolvedValueOnce({ data: { status: 'uploading' } })
				.mockResolvedValueOnce({ data: { status: 'available' } });

			const pollPromise = helpers.pollFileUntilAvailable.call(
				createMockExecuteFunction({}),
				'file-123',
				1000,
				0,
			);

			const result = await pollPromise;

			expect(apiRequestMock).toHaveBeenCalledTimes(2);
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '/files/file-123');
			expect(result).toBe('file-123');
		});

		it('should throw timeout error if file never becomes available', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			apiRequestMock.mockResolvedValue({ data: { status: 'processing' } });

			const promise = helpers.pollFileUntilAvailable.call(
				createMockExecuteFunction({}),
				'file-123',
				0,
			);

			await expect(promise).rejects.toThrow();
		});
	});

	describe('createAndUploadFile', () => {
		it('should create file entry, upload file, and poll until available', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			apiRequestMock
				.mockResolvedValueOnce(mockFileCreateResponse)
				.mockResolvedValueOnce({ data: { status: 'available' } });

			const mockExecuteFunction = createMockExecuteFunction({});
			const mockHttpRequest = vi.fn().mockResolvedValueOnce({});
			mockExecuteFunction.helpers.httpRequest = mockHttpRequest;
			const pollingFunctionMock = vi.fn().mockResolvedValueOnce(mockFileCreateResponse.data.id);

			const result = await helpers.createAndUploadFile.call(
				mockExecuteFunction,
				'test.png',
				Buffer.from('test'),
				'customer_upload',
				pollingFunctionMock,
			);

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/files', {
				fileName: 'test.png',
				fileType: 'customer_upload',
			});

			expect(mockHttpRequest).toHaveBeenCalledWith({
				method: 'PUT',
				url: mockFileCreateResponse.data.uploadUrl,
				body: Buffer.from('test'),
				headers: {
					'Content-Type': 'application/octet-stream',
				},
			});

			expect(pollingFunctionMock).toHaveBeenCalledWith(mockFileCreateResponse.data.id);

			expect(result).toBe(mockFileCreateResponse.data.id);
		});

		it('should throw error if file creation response is missing id or upload URL', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			apiRequestMock.mockResolvedValueOnce({});

			await expect(
				helpers.createAndUploadFile.call(
					createMockExecuteFunction({}),
					'test.pdf',
					Buffer.from('test'),
					'customer_upload',
				),
			).rejects.toThrow();
		});
	});

	describe('waitForFileInSession', () => {
		it('should resolve when file_upload_status event with available status is received', async () => {
			const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as Mock;
			const mockEvent = {
				event: 'file_upload_status',
				status: 'available',
				fileId: 'file-123',
			};
			waitForSessionEventMock.mockResolvedValueOnce(mockEvent);

			const mockExecuteFunction = createMockExecuteFunction({});

			await helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 1000);

			expect(waitForSessionEventMock).toHaveBeenCalledTimes(1);
			expect(waitForSessionEventMock).toHaveBeenCalledWith(
				'session-123',
				expect.any(Function),
				1000,
			);
		});

		it('should throw error when uploading a file with invalid file format', async () => {
			const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as Mock;
			const mockEvent = {
				event: 'file_upload_status',
				status: 'upload_failed',
				fileId: 'file-123',
				eventData: {
					error: 'Upload failed due to invalid file format',
				},
			};
			waitForSessionEventMock.mockResolvedValueOnce(mockEvent);

			const mockExecuteFunction = createMockExecuteFunction({});

			await expect(
				helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 1000),
			).rejects.toMatchObject({ description: 'Upload failed due to invalid file format' });
		});

		it('should throw error when upload_failed status is received', async () => {
			const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as Mock;
			const mockEvent = {
				fileId: 'file-123',
				event: 'file_upload_status',
				status: 'upload_failed',
				eventData: {
					error: 'Upload failed for File ID: file-123',
				},
			};
			waitForSessionEventMock.mockResolvedValueOnce(mockEvent);

			const mockExecuteFunction = createMockExecuteFunction({});

			// the service should throw an error description 'Upload failed for File ID: file-123'
			await expect(
				helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 1000),
			).rejects.toMatchObject({ description: 'Upload failed for File ID: file-123' });
		});

		it('should timeout if no matching event is received', async () => {
			const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as Mock;
			waitForSessionEventMock.mockRejectedValueOnce(new Error('Timeout reached'));

			const mockExecuteFunction = createMockExecuteFunction({});

			await expect(
				helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 100),
			).rejects.toThrow('Timeout reached');
		});
	});

	describe('pushFileToSession', () => {
		it('should push file to session and wait', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			const mockFileId = 'file-123';
			const mockSessionId = 'session-123';
			apiRequestMock.mockResolvedValueOnce({});

			// Mock waitForFileInSession
			const waitForFileInSessionMock = vi.fn().mockResolvedValueOnce({});

			// Call the function
			await helpers.pushFileToSession.call(
				createMockExecuteFunction({}),
				mockFileId,
				mockSessionId,
				waitForFileInSessionMock,
			);

			expect(apiRequestMock).toHaveBeenCalledWith('POST', `/files/${mockFileId}/push`, {
				sessionIds: [mockSessionId],
			});

			expect(waitForFileInSessionMock).toHaveBeenCalledWith(mockSessionId, mockFileId);
		});
	});

	describe('triggerFileInput', () => {
		it('should trigger file input in window', async () => {
			const apiRequestMock = transport.apiRequest as Mock;
			apiRequestMock.mockResolvedValueOnce({});
			const mockFileId = 'file-123';
			const mockWindowId = 'window-123';
			const mockSessionId = 'session-123';

			await helpers.triggerFileInput.call(createMockExecuteFunction({}), {
				fileId: mockFileId,
				windowId: mockWindowId,
				sessionId: mockSessionId,
				elementDescription: 'test',
				includeHiddenElements: false,
			});

			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				`/sessions/${mockSessionId}/windows/${mockWindowId}/file-input`,
				{
					fileId: mockFileId,
					elementDescription: 'test',
					includeHiddenElements: false,
				},
			);
		});
	});

	describe('createFileBuffer', () => {
		it('should create buffer from URL', async () => {
			const mockUrl = 'https://example.com/file.pdf';
			const mockBuffer = [1, 2, 3];

			// Mock http request
			const mockHttpRequest = vi.fn().mockResolvedValueOnce(mockBuffer);

			// Create mock execute function with http request helper
			const mockExecuteFunction = createMockExecuteFunction({});
			mockExecuteFunction.helpers.httpRequest = mockHttpRequest;

			const result = await helpers.createFileBuffer.call(mockExecuteFunction, 'url', mockUrl, 0);

			expect(mockHttpRequest).toHaveBeenCalledWith({
				url: mockUrl,
				json: false,
				encoding: 'arraybuffer',
			});
			expect(result).toBe(mockBuffer);
		});

		it('should create buffer from binary data', async () => {
			const mockBinaryPropertyName = 'data';
			const mockBuffer = [1, 2, 3];

			// Mock getBinaryDataBuffer
			const mockGetBinaryDataBuffer = vi.fn().mockResolvedValue(mockBuffer);

			// Create mock execute function with getBinaryDataBuffer helper
			const mockExecuteFunction = createMockExecuteFunction({});
			mockExecuteFunction.helpers.getBinaryDataBuffer = mockGetBinaryDataBuffer;

			const result = await helpers.createFileBuffer.call(
				mockExecuteFunction,
				'binary',
				mockBinaryPropertyName,
				0,
			);

			expect(mockGetBinaryDataBuffer).toHaveBeenCalledWith(0, mockBinaryPropertyName);
			expect(result).toBe(mockBuffer);
		});

		it('should throw error for unsupported source type', async () => {
			await expect(
				helpers.createFileBuffer.call(
					createMockExecuteFunction({}),
					'invalid-source',
					'test-value',
					0,
				),
			).rejects.toThrow();
		});
	});
});
