/**
 * Unit tests for deleteService — path traversal protection (H-08)
 */

import path from 'path';

// We need to mock fs and the logger before importing deleteService
jest.mock('fs', () => ({
	existsSync: jest.fn(),
	promises: {
		unlink: jest.fn(),
	},
}));

jest.mock('#services/file-system/logService', () => ({
	LoggingService: jest.fn().mockImplementation(() => ({
		logErrorAsync: jest.fn().mockResolvedValue(undefined),
	})),
}));

import fs from 'fs';
import { deleteFile } from '#services/file-system/deleteService';

const mockExistsSync = fs.existsSync as jest.Mock;
const mockUnlink = fs.promises.unlink as jest.Mock;

beforeEach(() => {
	jest.clearAllMocks();
	process.env.UPLOAD_PATH = '/app/storage/uploads';
	mockExistsSync.mockReturnValue(false); // Default: file does not exist
});

afterEach(() => {
	delete process.env.UPLOAD_PATH;
});

describe('deleteService › path traversal protection (H-08)', () => {
	it('deletes a valid file path without error', async () => {
		mockExistsSync.mockReturnValue(true);
		mockUnlink.mockResolvedValueOnce(undefined);

		const result = await deleteFile({ filePath: '/images/photo.jpg' });

		expect(result).toBe(true);
		expect(mockUnlink).toHaveBeenCalledWith(path.resolve('/app/storage/uploads', 'images/photo.jpg'));
	});

	it('throws when filePath contains path traversal (../../)', async () => {
		await expect(deleteFile({ filePath: '/../../etc/passwd' })).rejects.toThrow(/path traversal/i);

		expect(mockUnlink).not.toHaveBeenCalled();
	});

	it('throws when filePath resolves outside upload directory', async () => {
		await expect(deleteFile({ filePath: '../../../etc/hosts' })).rejects.toThrow(/path traversal/i);

		expect(mockUnlink).not.toHaveBeenCalled();
	});

	it('throws when compressed variant path has traversal', async () => {
		mockExistsSync.mockReturnValue(true);
		mockUnlink.mockResolvedValue(undefined);

		await expect(
			deleteFile({
				filePath: '/images/photo.jpg',
				compressInfo: { sm: '../../etc/shadow' },
			}),
		).rejects.toThrow(/path traversal/i);
	});

	it('deletes file with valid compress info', async () => {
		mockExistsSync.mockReturnValue(true);
		mockUnlink.mockResolvedValue(undefined);

		const result = await deleteFile({
			filePath: '/images/photo.jpg',
			compressInfo: { sm: '/images/photo-sm.webp', md: '/images/photo-md.webp' },
		});

		expect(result).toBe(true);
		expect(mockUnlink).toHaveBeenCalledTimes(3);
	});

	it('returns true when file does not exist (idempotent delete)', async () => {
		// Implementation now attempts unlink and treats ENOENT as success (avoids existsSync)
		const enoentError = Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
		mockUnlink.mockRejectedValue(enoentError);

		const result = await deleteFile({ filePath: '/images/nonexistent.jpg' });

		expect(result).toBe(true);
		expect(mockUnlink).toHaveBeenCalledTimes(1);
	});

	it('returns false when unlink fails', async () => {
		mockExistsSync.mockReturnValue(true);
		mockUnlink.mockRejectedValueOnce(new Error('Permission denied'));

		const result = await deleteFile({ filePath: '/images/locked.jpg' });

		expect(result).toBe(false);
	});
});
