import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
import { backupsService } from "../backups.service";
import { createTestVolume } from "~/test/helpers/volume";
import { createTestBackupSchedule } from "~/test/helpers/backup";
import { createTestRepository } from "~/test/helpers/repository";
import { generateBackupOutput } from "~/test/helpers/restic";
import { getVolumePath } from "../../volumes/helpers";
import { restic } from "~/server/utils/restic";
import path from "node:path";

const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));

beforeEach(() => {
	backupMock.mockClear();
	spyOn(restic, "backup").mockImplementation(backupMock);
	spyOn(restic, "forget").mockImplementation(mock(() => Promise.resolve({ success: true })));
});

afterEach(() => {
	mock.restore();
});

describe("executeBackup - include / exclude patterns", () => {
	test("should correctly build include and exclude patterns", async () => {
		// arrange
		const volume = await createTestVolume();
		const repository = await createTestRepository();
		const volumePath = getVolumePath(volume);

		const schedule = await createTestBackupSchedule({
			volumeId: volume.id,
			repositoryId: repository.id,
			includePatterns: ["*.zip", "/Photos", "!/Temp", "!*.log"],
			excludePatterns: [".DS_Store", "/Config", "!/Important", "!*.tmp"],
			excludeIfPresent: [".nobackup"],
		});

		// act
		await backupsService.executeBackup(schedule.id);

		// assert
		expect(backupMock).toHaveBeenCalledWith(
			expect.anything(),
			volumePath,
			expect.objectContaining({
				include: ["*.zip", path.join(volumePath, "Photos"), `!${path.join(volumePath, "Temp")}`, "!*.log"],
				exclude: [".DS_Store", path.join(volumePath, "Config"), `!${path.join(volumePath, "Important")}`, "!*.tmp"],
				excludeIfPresent: [".nobackup"],
			}),
		);
	});

	test("should not join with volume path if pattern already starts with it", async () => {
		// arrange
		const volume = await createTestVolume();
		const volumePath = getVolumePath(volume);
		const repository = await createTestRepository();

		const alreadyJoinedInclude = path.join(volumePath, "already/joined");
		const alreadyJoinedExclude = path.join(volumePath, "already/excluded");

		const schedule = await createTestBackupSchedule({
			volumeId: volume.id,
			repositoryId: repository.id,
			includePatterns: [alreadyJoinedInclude],
			excludePatterns: [alreadyJoinedExclude],
		});

		// act
		await backupsService.executeBackup(schedule.id);

		// assert
		expect(backupMock).toHaveBeenCalledWith(
			expect.anything(),
			volumePath,
			expect.objectContaining({
				include: [alreadyJoinedInclude],
				exclude: [alreadyJoinedExclude],
			}),
		);
	});

	test("should correctly mix relative and absolute patterns", async () => {
		// arrange
		const volume = await createTestVolume();
		const volumePath = getVolumePath(volume);
		const repository = await createTestRepository();

		const alreadyJoinedInclude = path.join(volumePath, "already/joined");
		const relativeInclude = "relative/include";
		const anchoredInclude = "/anchored/include";

		const schedule = await createTestBackupSchedule({
			volumeId: volume.id,
			repositoryId: repository.id,
			includePatterns: [alreadyJoinedInclude, relativeInclude, anchoredInclude],
		});

		// act
		await backupsService.executeBackup(schedule.id);

		// assert
		expect(backupMock).toHaveBeenCalledWith(
			expect.anything(),
			volumePath,
			expect.objectContaining({
				include: [alreadyJoinedInclude, relativeInclude, path.join(volumePath, "anchored/include")],
			}),
		);
	});

	test("should handle empty include and exclude patterns", async () => {
		// arrange
		const volume = await createTestVolume();
		const repository = await createTestRepository();
		const schedule = await createTestBackupSchedule({
			volumeId: volume.id,
			repositoryId: repository.id,
			includePatterns: [],
			excludePatterns: [],
		});

		// act
		await backupsService.executeBackup(schedule.id);

		// assert
		expect(backupMock).toHaveBeenCalledWith(
			expect.anything(),
			getVolumePath(volume),
			expect.not.objectContaining({
				include: expect.anything(),
				exclude: expect.anything(),
			}),
		);
	});
});
