import fs from "node:fs/promises";
import path from "node:path";
import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";

const OUTPUT_BASE = "./__test__/drop/zips/output";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => {
	const actual = await importOriginal();
	return {
		// @ts-ignore
		...actual,
		paths: () => ({
			// @ts-ignore
			...actual.paths(),
			BASE_PATH: OUTPUT_BASE,
			APPLICATIONS_PATH: OUTPUT_BASE,
		}),
	};
});

if (typeof window === "undefined") {
	const undici = require("undici");
	globalThis.File = undici.File as any;
	globalThis.FileList = undici.FileList as any;
}

const baseApp: ApplicationNested = {
	railpackVersion: "0.15.4",
	applicationId: "",
	previewLabels: [],
	createEnvFile: true,
	bitbucketRepositorySlug: "",
	herokuVersion: "",
	giteaBranch: "",
	buildServerId: "",
	buildRegistryId: "",
	buildRegistry: null,
	args: [],
	giteaBuildPath: "",
	previewRequireCollaboratorPermissions: false,
	giteaId: "",
	giteaOwner: "",
	giteaRepository: "",
	cleanCache: false,
	watchPaths: [],
	rollbackRegistryId: "",
	rollbackRegistry: null,
	deployments: [],
	enableSubmodules: false,
	applicationStatus: "done",
	triggerType: "push",
	appName: "",
	autoDeploy: true,
	endpointSpecSwarm: null,
	serverId: "",
	registryUrl: "",
	branch: null,
	dockerBuildStage: "",
	isPreviewDeploymentsActive: false,
	previewBuildArgs: null,
	previewBuildSecrets: null,
	previewCertificateType: "none",
	previewCustomCertResolver: null,
	previewEnv: null,
	previewHttps: false,
	previewPath: "/",
	previewPort: 3000,
	previewLimit: 0,
	previewWildcard: "",
	environment: {
		env: "",
		isDefault: false,
		environmentId: "",
		name: "",
		createdAt: "",
		description: "",
		projectId: "",
		project: {
			env: "",
			organizationId: "",
			name: "",
			description: "",
			createdAt: "",
			projectId: "",
		},
	},
	buildArgs: null,
	buildSecrets: null,
	buildPath: "/",
	gitlabPathNamespace: "",
	buildType: "nixpacks",
	bitbucketBranch: "",
	bitbucketBuildPath: "",
	bitbucketId: "",
	bitbucketRepository: "",
	bitbucketOwner: "",
	githubId: "",
	gitlabProjectId: 0,
	gitlabBranch: "",
	gitlabBuildPath: "",
	gitlabId: "",
	gitlabRepository: "",
	gitlabOwner: "",
	command: null,
	cpuLimit: null,
	cpuReservation: null,
	createdAt: "",
	customGitBranch: "",
	customGitBuildPath: "",
	customGitSSHKeyId: null,
	customGitUrl: "",
	description: "",
	dockerfile: null,
	dockerImage: null,
	dropBuildPath: null,
	environmentId: "",
	enabled: null,
	env: null,
	icon: null,
	healthCheckSwarm: null,
	labelsSwarm: null,
	memoryLimit: null,
	memoryReservation: null,
	modeSwarm: null,
	mounts: [],
	name: "",
	networkSwarm: null,
	owner: null,
	password: null,
	placementSwarm: null,
	ports: [],
	publishDirectory: null,
	isStaticSpa: null,
	redirects: [],
	refreshToken: "",
	registry: null,
	registryId: null,
	replicas: 1,
	repository: null,
	restartPolicySwarm: null,
	rollbackConfigSwarm: null,
	security: [],
	sourceType: "git",
	subtitle: null,
	title: null,
	updateConfigSwarm: null,
	username: null,
	dockerContextPath: null,
	rollbackActive: false,
	stopGracePeriodSwarm: null,
	ulimitsSwarm: null,
};

/**
 * GHSA-66v7-g3fh-47h3: Remote Code Execution through Path Traversal.
 * Validates the exact PoC: ZIP with path traversal entry ../../../../../etc/cron.d/malicious-cron
 * plus cover files (package.json, index.js). unzipDrop must reject and never write outside output.
 */
describe("GHSA-66v7-g3fh-47h3 path traversal RCE", () => {
	beforeAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});
	afterAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	it("rejects PoC ZIP: traversal ../../../../../etc/cron.d/malicious-cron + package.json + index.js", async () => {
		baseApp.appName = "ghsa-rce";
		// PoC payload: same entry name as advisory (Python zipfile keeps it; AdmZip normalizes on add → use placeholder + replace)
		const traversalEntry = "../../../../../etc/cron.d/malicious-cron";
		const cronPayload = "* * * * * root id\n";
		const placeholder = "x".repeat(traversalEntry.length);
		const zip = new AdmZip();
		zip.addFile(
			"package.json",
			Buffer.from('{"name": "app", "version": "1.0.0"}'),
		);
		zip.addFile("index.js", Buffer.from('console.log("Application");'));
		zip.addFile(placeholder, Buffer.from(cronPayload));
		let buf = Buffer.from(zip.toBuffer());
		buf = Buffer.from(
			buf.toString("binary").split(placeholder).join(traversalEntry),
			"binary",
		);
		const file = new File([buf as unknown as ArrayBuffer], "exploit.zip");
		await expect(unzipDrop(file, baseApp)).rejects.toThrow(
			/Path traversal detected.*resolved path escapes output directory/,
		);
	});
});

describe("security: existing symlink escape", () => {
	beforeAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	afterAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	it("should NOT write outside base when directory is a symlink", async () => {
		const appName = "symlink-existing";
		const output = path.join(APPLICATIONS_PATH, appName, "code");
		await fs.mkdir(output, { recursive: true });

		// outside target (attacker wants to write here)
		const outside = path.join(APPLICATIONS_PATH, "..", "outside");
		await fs.mkdir(outside, { recursive: true });

		// attacker-controlled symlink inside project
		await fs.symlink(outside, path.join(output, "logs"));

		// zip looks totally harmless
		const zip = new AdmZip();
		zip.addFile("logs/pwned.txt", Buffer.from("owned"));

		const file = new File([zip.toBuffer() as any], "exploit.zip");

		await unzipDrop(file, { ...baseApp, appName });

		// if vulnerable -> file exists outside sandbox
		const escaped = await fs
			.readFile(path.join(outside, "pwned.txt"), "utf8")
			.then(() => true)
			.catch(() => false);

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

describe("security: zip symlink entry blocked", () => {
	beforeAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	afterAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	it("rejects zip containing real symlink entry", async () => {
		const appName = "zip-symlink";

		const zipBuffer = await fs.readFile(
			path.join(__dirname, "./zips/payload/symlink-entry.zip"),
		);

		const file = new File([zipBuffer as any], "exploit.zip");

		await expect(unzipDrop(file, { ...baseApp, appName })).rejects.toThrow(
			/Dangerous node entries are not allowed/,
		);
	});
});

describe("unzipDrop path under output (no traversal)", () => {
	beforeAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});
	afterAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	it("allows entry etc/cron.d/malicious-cron when under output (no path traversal)", async () => {
		baseApp.appName = "cron-under-output";
		const zip = new AdmZip();
		zip.addFile(
			"etc/cron.d/malicious-cron",
			Buffer.from("* * * * * root id\n"),
		);
		zip.addFile("package.json", Buffer.from('{"name":"app"}'));
		const file = new File(
			[zip.toBuffer() as unknown as ArrayBuffer],
			"app.zip",
		);
		const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
		await unzipDrop(file, baseApp);
		const content = await fs.readFile(
			path.join(outputPath, "etc/cron.d/malicious-cron"),
			"utf8",
		);
		expect(content).toBe("* * * * * root id\n");
	});
});

describe("security: traversal inside BASE_PATH (sandbox escape)", () => {
	beforeAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	afterAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	it("should NOT allow writing outside application directory but inside BASE_PATH", async () => {
		const appName = "sandbox-escape";

		const base = APPLICATIONS_PATH.replace("/applications", "");
		const output = path.join(APPLICATIONS_PATH, appName, "code");

		await fs.mkdir(output, { recursive: true });

		// attacker writes into traefik config inside base
		const zip = new AdmZip();
		zip.addFile(
			"../../../traefik/dynamic/evil.yml",
			Buffer.from("pwned: true"),
		);

		const file = new File([zip.toBuffer() as any], "exploit.zip");

		await unzipDrop(file, { ...baseApp, appName });

		const escapedPath = path.join(base, "traefik/dynamic/evil.yml");

		const exists = await fs
			.readFile(escapedPath)
			.then(() => true)
			.catch(() => false);

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

describe("unzipDrop using real zip files", () => {
	// const { APPLICATIONS_PATH } = paths();
	beforeAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	afterAll(async () => {
		await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
	});

	it("should correctly extract a zip with a single root folder", async () => {
		baseApp.appName = "single-file";
		// const appName = "single-file";
		try {
			const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
			const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
			const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
			const file = new File([zipBuffer], "single.zip");
			await unzipDrop(file, baseApp);
			const files = await fs.readdir(outputPath, { withFileTypes: true });
			expect(files.some((f) => f.name === "test.txt")).toBe(true);
		} catch (err) {
		} finally {
		}
	});
});

// 	it("should correctly extract a zip with a single root folder and a subfolder", async () => {
// 		baseApp.appName = "folderwithfile";
// 		// const appName = "folderwithfile";
// 		const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// 		const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");

// 		const zipBuffer = zip.toBuffer();
// 		const file = new File([zipBuffer], "single.zip");
// 		await unzipDrop(file, baseApp);

// 		const files = await fs.readdir(outputPath, { withFileTypes: true });
// 		expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
// 	});

// 	it("should correctly extract a zip with multiple root folders", async () => {
// 		baseApp.appName = "two-folders";
// 		// const appName = "two-folders";
// 		const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// 		const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");

// 		const zipBuffer = zip.toBuffer();
// 		const file = new File([zipBuffer], "single.zip");
// 		await unzipDrop(file, baseApp);

// 		const files = await fs.readdir(outputPath, { withFileTypes: true });

// 		expect(files.some((f) => f.name === "folder1")).toBe(true);
// 		expect(files.some((f) => f.name === "folder2")).toBe(true);
// 	});

// 	it("should correctly extract a zip with a single root with a file", async () => {
// 		baseApp.appName = "nested";
// 		// const appName = "nested";
// 		const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// 		const zip = new AdmZip("./__test__/drop/zips/nested.zip");

// 		const zipBuffer = zip.toBuffer();
// 		const file = new File([zipBuffer], "single.zip");
// 		await unzipDrop(file, baseApp);

// 		const files = await fs.readdir(outputPath, { withFileTypes: true });

// 		expect(files.some((f) => f.name === "folder1")).toBe(true);
// 		expect(files.some((f) => f.name === "folder2")).toBe(true);
// 		expect(files.some((f) => f.name === "folder3")).toBe(true);
// 	});

// 	it("should correctly extract a zip with a single root with a folder", async () => {
// 		baseApp.appName = "folder-with-sibling-file";
// 		// const appName = "folder-with-sibling-file";
// 		const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// 		const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");

// 		const zipBuffer = zip.toBuffer();
// 		const file = new File([zipBuffer], "single.zip");
// 		await unzipDrop(file, baseApp);

// 		const files = await fs.readdir(outputPath, { withFileTypes: true });

// 		expect(files.some((f) => f.name === "folder1")).toBe(true);
// 		expect(files.some((f) => f.name === "test.txt")).toBe(true);
// 	});
// });
