import { describe, expect, it, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, writeFile, symlink, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ErrorCode } from "@pocketcoder/shared/constants";
import { AppError } from "../../errors/index.js";
import { validateProjectPath, validateParentPath } from "../path-sandbox.js";
describe("path-sandbox", () => {
    let projectRoot;
    beforeEach(async () => {
        // Use realpath to get the canonical path (handles macOS /var -> /private/var symlink)
        const tempDir = await mkdtemp(join(tmpdir(), "pocketcoder-sandbox-test-"));
        projectRoot = await realpath(tempDir);
        // Create test directory structure
        await mkdir(join(projectRoot, "src"), { recursive: true });
        await mkdir(join(projectRoot, "src", "nested"), { recursive: true });
        await writeFile(join(projectRoot, "src", "index.ts"), "export {};");
        await writeFile(join(projectRoot, "src", "nested", "deep.ts"), "export {};");
    });
    afterEach(async () => {
        await rm(projectRoot, { recursive: true, force: true });
    });
    describe("validateProjectPath", () => {
        it("should allow valid relative paths", async () => {
            const result = await validateProjectPath(projectRoot, "src/index.ts");
            expect(result).toBe(join(projectRoot, "src", "index.ts"));
        });
        it("should allow nested relative paths", async () => {
            const result = await validateProjectPath(projectRoot, "src/nested/deep.ts");
            expect(result).toBe(join(projectRoot, "src", "nested", "deep.ts"));
        });
        it("should allow the project root itself", async () => {
            const result = await validateProjectPath(projectRoot, ".");
            expect(result).toBe(projectRoot);
        });
        it("should reject ../.. traversal attempts", async () => {
            await expect(validateProjectPath(projectRoot, "../../etc/passwd")).rejects.toThrow(AppError);
            try {
                await validateProjectPath(projectRoot, "../../etc/passwd");
            }
            catch (err) {
                expect(err).toBeInstanceOf(AppError);
                expect(err.code).toBe(ErrorCode.OUTSIDE_PROJECT_ROOT);
            }
        });
        it("should reject absolute paths outside project", async () => {
            await expect(validateProjectPath(projectRoot, "/etc/passwd")).rejects.toThrow(AppError);
            try {
                await validateProjectPath(projectRoot, "/etc/passwd");
            }
            catch (err) {
                expect(err).toBeInstanceOf(AppError);
                expect(err.code).toBe(ErrorCode.OUTSIDE_PROJECT_ROOT);
            }
        });
        it("should reject traversal hidden in middle of path", async () => {
            await expect(validateProjectPath(projectRoot, "src/../../../etc/passwd")).rejects.toThrow(AppError);
        });
        it("should handle symlinks safely - reject symlinks pointing outside", async () => {
            // Create a symlink that points outside the project
            const symlinkPath = join(projectRoot, "escape-link");
            await symlink("/tmp", symlinkPath);
            await expect(validateProjectPath(projectRoot, "escape-link")).rejects.toThrow(AppError);
        });
        it("should allow symlinks pointing within project", async () => {
            const symlinkPath = join(projectRoot, "link-to-src");
            await symlink(join(projectRoot, "src"), symlinkPath);
            const result = await validateProjectPath(projectRoot, "link-to-src");
            expect(result).toBe(join(projectRoot, "src"));
        });
        it("should normalize paths correctly", async () => {
            const result = await validateProjectPath(projectRoot, "src/../src/index.ts");
            expect(result).toBe(join(projectRoot, "src", "index.ts"));
        });
        it("should handle non-existent files within project", async () => {
            const result = await validateProjectPath(projectRoot, "does-not-exist.txt");
            expect(result).toBe(join(projectRoot, "does-not-exist.txt"));
        });
    });
    describe("validateParentPath", () => {
        it("should allow valid paths for new files", async () => {
            const result = await validateParentPath(projectRoot, "src/new-file.ts");
            expect(result).toBe(join(projectRoot, "src", "new-file.ts"));
        });
        it("should reject traversal attempts for write paths", async () => {
            await expect(validateParentPath(projectRoot, "../../escape.txt")).rejects.toThrow(AppError);
        });
        it("should reject absolute paths", async () => {
            await expect(validateParentPath(projectRoot, "/etc/evil.txt")).rejects.toThrow(AppError);
        });
        it("should allow deeply nested new paths", async () => {
            const result = await validateParentPath(projectRoot, "a/b/c/d/new.ts");
            expect(result).toBe(join(projectRoot, "a", "b", "c", "d", "new.ts"));
        });
    });
});
//# sourceMappingURL=path-sandbox.test.js.map