// biome-ignore-all lint/suspicious/noExplicitAny: Allow any
import assert from "node:assert";
import { readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, it } from "node:test";
import fastifyMultipart from "@fastify/multipart";
import { cleanupTestDir, createTempTestDir, createTestFastifyInstance, createTestFileStructure, } from "../test-utils/setup.js";
import { createFsRoutes } from "./fs.js";
describe("Filesystem Routes", () => {
    let app;
    let testDir;
    beforeEach(async () => {
        testDir = await createTempTestDir("fs-test-");
        await createTestFileStructure(testDir);
        app = await createTestFastifyInstance();
        await app.register(fastifyMultipart);
        await app.register(createFsRoutes, {
            prefix: "/fs",
            sourceDirectory: testDir,
        });
        await app.ready();
    });
    afterEach(async () => {
        await app.close();
        await cleanupTestDir(testDir);
    });
    describe("GET /fs/dir", () => {
        it("should list root directories", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/dir?path=/",
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            assert.strictEqual(body.path, "/");
            assert.ok(Array.isArray(body.result));
            // Check for expected directories
            const paths = body.result.map((item) => item[0]);
            assert.ok(paths.includes("/config"));
            assert.ok(paths.includes("/modules"));
            assert.ok(paths.includes("/schemas"));
        });
        it("should list files in a specific directory", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/dir?path=/config",
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            assert.strictEqual(body.path, "/config");
            // Check for expected file
            const paths = body.result.map((item) => item[0]);
            assert.ok(paths.includes("/config/routes.json"));
        });
        it("should list files recursively", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/dir?path=/&recursive=true",
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            // Should include both directories and files
            const items = body.result;
            const files = items.filter(([, type]) => type === 1); // Type 1 = File
            const dirs = items.filter(([, type]) => type === 2); // Type 2 = Directory
            assert.ok(files.length > 0);
            assert.ok(dirs.length > 0);
        });
        it("should return correct file types", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/dir?path=/config",
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            // Find the routes.json file entry
            const routesEntry = body.result.find((item) => item[0] === "/config/routes.json");
            assert.ok(routesEntry);
            assert.strictEqual(routesEntry[1], 1); // Type 1 = File
        });
    });
    describe("GET /fs/file", () => {
        it("should read file content", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(response.statusCode, 200);
            // Check content-type includes text/plain (charset may or may not be added)
            const contentType = response.headers["content-type"];
            assert.ok(contentType &&
                typeof contentType === "string" &&
                contentType.includes("text/plain"));
            const content = JSON.parse(response.body);
            assert.deepStrictEqual(content, { routes: [] });
        });
        it("should include Last-Modified header", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(response.statusCode, 200);
            assert.ok(response.headers["last-modified"]);
        });
        it("should handle files with leading slash", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/file?path=/modules/my-handler.ts",
            });
            assert.strictEqual(response.statusCode, 200);
            assert.ok(response.body.includes("export default"));
        });
        it("should return 404 for non-existent file", async () => {
            const response = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/nonexistent.json",
            });
            assert.strictEqual(response.statusCode, 404);
            const body = JSON.parse(response.body);
            const errorMsg = body.detail || body.message || body.title || "";
            assert.ok(errorMsg.toLowerCase().includes("cannot find") ||
                errorMsg.toLowerCase().includes("not found"));
        });
    });
    describe("POST /fs/file", () => {
        it("should create new file", async () => {
            const newContent = "console.log('test');";
            const response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/modules/new-file.ts",
                payload: newContent,
                headers: {
                    "content-type": "text/plain",
                },
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            assert.ok(body.buildId);
            // Verify file was created
            const content = await readFile(path.join(testDir, "modules", "new-file.ts"), "utf-8");
            assert.strictEqual(content, newContent);
        });
        it("should update existing file", async () => {
            const newContent = JSON.stringify({ routes: ["new route"] }, null, 2);
            // Get the current mtime for the If-Unmodified-Since header
            const stats = await stat(path.join(testDir, "config", "routes.json"));
            const response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: newContent,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": stats.mtime.toUTCString(),
                },
            });
            assert.strictEqual(response.statusCode, 200);
            // Verify file was updated
            const content = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(content, newContent);
        });
        it("should detect conflicts when file modified", async () => {
            // Get the original file stats
            const oldStats = await stat(path.join(testDir, "config", "routes.json"));
            // Wait to ensure timestamp difference beyond conflict tolerance (2000ms + buffer)
            await new Promise((resolve) => setTimeout(resolve, 2500));
            // Modify file externally with different content
            await writeFile(path.join(testDir, "config", "routes.json"), JSON.stringify({ routes: ["externally modified"] }, null, 2), "utf-8");
            // Try to save with old timestamp
            const response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: JSON.stringify({ routes: ["my changes"] }, null, 2),
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": oldStats.mtime.toUTCString(),
                },
            });
            assert.strictEqual(response.statusCode, 412); // Precondition Failed
        });
        it("should allow force save with x-force-save header", async () => {
            // Modify the file to simulate external change
            const oldStats = await stat(path.join(testDir, "config", "routes.json"));
            await new Promise((resolve) => setTimeout(resolve, 100));
            await writeFile(path.join(testDir, "config", "routes.json"), JSON.stringify({ routes: ["externally modified"] }, null, 2), "utf-8");
            // Force save should work despite conflict
            const newContent = JSON.stringify({ routes: ["forced changes"] }, null, 2);
            const response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: newContent,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": oldStats.mtime.toUTCString(),
                    "x-force-save": "true",
                },
            });
            assert.strictEqual(response.statusCode, 200);
            // Verify forced content was saved
            const content = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(content, newContent);
        });
        it("should create parent directories if needed", async () => {
            const response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/nested/deep/file.json",
                payload: "{}",
                headers: {
                    "content-type": "text/plain",
                },
            });
            assert.strictEqual(response.statusCode, 200);
            // Verify file was created
            const content = await readFile(path.join(testDir, "config", "nested", "deep", "file.json"), "utf-8");
            assert.strictEqual(content, "{}");
        });
    });
    describe("POST /fs/files (multipart)", () => {
        function createMultipartPayload(files) {
            const boundary = `----WebKitFormBoundary${Math.random().toString(36).slice(2)}`;
            let payload = "";
            for (const file of files) {
                payload += `--${boundary}\r\n`;
                payload += `Content-Disposition: form-data; name="file"; filename="${file.filename}"\r\n`;
                payload += `Content-Type: application/octet-stream\r\n`;
                // Add custom headers if provided
                if (file.headers) {
                    for (const [key, value] of Object.entries(file.headers)) {
                        payload += `${key}: ${value}\r\n`;
                    }
                }
                payload += `\r\n`;
                payload += `${file.content}\r\n`;
            }
            payload += `--${boundary}--\r\n`;
            return { payload, boundary };
        }
        it("should upload multiple files", async () => {
            const { payload, boundary } = createMultipartPayload([
                { filename: "/config/file1.txt", content: "content1" },
                { filename: "/modules/file2.txt", content: "content2" },
            ]);
            const response = await app.inject({
                method: "POST",
                url: "/fs/files",
                payload,
                headers: {
                    "content-type": `multipart/form-data; boundary=${boundary}`,
                },
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            assert.ok(body.buildId);
            // Verify files were created
            const content1 = await readFile(path.join(testDir, "config", "file1.txt"), "utf-8");
            assert.strictEqual(content1, "content1");
            const content2 = await readFile(path.join(testDir, "modules", "file2.txt"), "utf-8");
            assert.strictEqual(content2, "content2");
        });
        it("should detect conflicts in multipart upload", async () => {
            // Create and modify a file
            await writeFile(path.join(testDir, "config", "test.txt"), "original", "utf-8");
            const oldStats = await stat(path.join(testDir, "config", "test.txt"));
            // Wait to ensure timestamp difference beyond conflict tolerance (2000ms + buffer)
            await new Promise((resolve) => setTimeout(resolve, 2500));
            // Modify externally with different content
            await writeFile(path.join(testDir, "config", "test.txt"), "externally modified", "utf-8");
            const headerKey = `x-if-unmodified-since-${encodeURIComponent("/config/test.txt")}`;
            const { payload, boundary } = createMultipartPayload([
                { filename: "/config/test.txt", content: "my changes" },
            ]);
            const response = await app.inject({
                method: "POST",
                url: "/fs/files",
                payload,
                headers: {
                    "content-type": `multipart/form-data; boundary=${boundary}`,
                    [headerKey]: oldStats.mtime.toUTCString(),
                },
            });
            assert.strictEqual(response.statusCode, 412); // Precondition Failed
            const body = JSON.parse(response.body);
            const errorMsg = (body.detail ||
                body.message ||
                body.title ||
                "").toLowerCase();
            assert.ok(errorMsg.includes("modified") || errorMsg.includes("precondition"), `Expected precondition failed error but got: ${errorMsg}`);
        });
        it("should return 400 for non-multipart request", async () => {
            const response = await app.inject({
                method: "POST",
                url: "/fs/files",
                payload: "not multipart",
                headers: {
                    "content-type": "text/plain",
                },
            });
            assert.strictEqual(response.statusCode, 400);
        });
        it("should handle multiple files with no conflicts", async () => {
            const { payload, boundary } = createMultipartPayload([
                { filename: "/config/multi1.txt", content: "file1" },
                { filename: "/config/multi2.txt", content: "file2" },
                { filename: "/modules/multi3.txt", content: "file3" },
            ]);
            const response = await app.inject({
                method: "POST",
                url: "/fs/files",
                payload,
                headers: {
                    "content-type": `multipart/form-data; boundary=${boundary}`,
                },
            });
            assert.strictEqual(response.statusCode, 200);
            // Verify all files were created
            const content1 = await readFile(path.join(testDir, "config", "multi1.txt"), "utf-8");
            assert.strictEqual(content1, "file1");
            const content2 = await readFile(path.join(testDir, "config", "multi2.txt"), "utf-8");
            assert.strictEqual(content2, "file2");
            const content3 = await readFile(path.join(testDir, "modules", "multi3.txt"), "utf-8");
            assert.strictEqual(content3, "file3");
        });
    });
    describe("PATCH /fs/file", () => {
        it("should rename file", async () => {
            const response = await app.inject({
                method: "PATCH",
                url: "/fs/file?path=/modules/my-handler.ts",
                payload: {
                    newPath: "/modules/renamed-handler.ts",
                },
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            assert.ok(body.buildId);
            // Verify file was renamed
            const content = await readFile(path.join(testDir, "modules", "renamed-handler.ts"), "utf-8");
            assert.ok(content.includes("export default"));
        });
        it("should return 404 for non-existent file", async () => {
            const response = await app.inject({
                method: "PATCH",
                url: "/fs/file?path=/modules/nonexistent.ts",
                payload: {
                    newPath: "/modules/renamed.ts",
                },
            });
            assert.strictEqual(response.statusCode, 404);
        });
    });
    describe("DELETE /fs/delete", () => {
        it("should delete file", async () => {
            const response = await app.inject({
                method: "DELETE",
                url: "/fs/delete?path=/modules/my-handler.ts",
            });
            assert.strictEqual(response.statusCode, 200);
            const body = JSON.parse(response.body);
            assert.ok(body.buildId);
            // Verify file was deleted
            try {
                await readFile(path.join(testDir, "modules", "my-handler.ts"), "utf-8");
                assert.fail("File should have been deleted");
            }
            catch (err) {
                assert.strictEqual(err.code, "ENOENT");
            }
        });
        it("should return 404 for non-existent file", async () => {
            const response = await app.inject({
                method: "DELETE",
                url: "/fs/delete?path=/modules/nonexistent.ts",
            });
            assert.strictEqual(response.statusCode, 404);
        });
    });
    describe("POST /fs/dir", () => {
        it("should create directory", async () => {
            const response = await app.inject({
                method: "POST",
                url: "/fs/dir?path=/config/new-dir",
            });
            assert.strictEqual(response.statusCode, 201);
            // Verify directory was created
            const stats = await stat(path.join(testDir, "config", "new-dir"));
            assert.ok(stats.isDirectory());
        });
        it("should create nested directories", async () => {
            const response = await app.inject({
                method: "POST",
                url: "/fs/dir?path=/config/nested/deep/dir",
            });
            assert.strictEqual(response.statusCode, 201);
            // Verify nested directories were created
            const stats = await stat(path.join(testDir, "config", "nested", "deep", "dir"));
            assert.ok(stats.isDirectory());
        });
        it("should succeed if directory already exists", async () => {
            // Create directory first
            await app.inject({
                method: "POST",
                url: "/fs/dir?path=/config/existing-dir",
            });
            // Try to create again
            const response = await app.inject({
                method: "POST",
                url: "/fs/dir?path=/config/existing-dir",
            });
            assert.strictEqual(response.statusCode, 201);
        });
    });
    describe("Multi-save workflows", () => {
        it("should handle list-read-save flow", async () => {
            // 1. List files
            const listResponse = await app.inject({
                method: "GET",
                url: "/fs/dir?path=/config",
            });
            assert.strictEqual(listResponse.statusCode, 200);
            const listBody = JSON.parse(listResponse.body);
            const paths = listBody.result.map((item) => item[0]);
            assert.ok(paths.includes("/config/routes.json"));
            // 2. Read the file
            const readResponse = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(readResponse.statusCode, 200);
            const lastModified = readResponse.headers["last-modified"];
            assert.ok(lastModified);
            // 3. Save the file with the Last-Modified timestamp
            const newContent = JSON.stringify({ routes: ["test route"] }, null, 2);
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: newContent,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": lastModified,
                },
            });
            assert.strictEqual(saveResponse.statusCode, 200);
            // Verify file was saved
            const content = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(content, newContent);
        });
        it("should detect conflict when file is edited externally between saves", async () => {
            // 1. Read the file and get its timestamp
            const readResponse = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(readResponse.statusCode, 200);
            const lastModified = readResponse.headers["last-modified"];
            // Wait to ensure timestamp difference beyond conflict tolerance (2000ms + buffer)
            await new Promise((resolve) => setTimeout(resolve, 2500));
            // 2. Edit file externally (simulating another process/user)
            await writeFile(path.join(testDir, "config", "routes.json"), JSON.stringify({ routes: ["external edit"] }, null, 2), "utf-8");
            // 3. Try to save with old timestamp - should fail with 412
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: JSON.stringify({ routes: ["my changes"] }, null, 2),
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": lastModified,
                },
            });
            assert.strictEqual(saveResponse.statusCode, 412);
            // Verify the external edit is still there (not overwritten)
            const content = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(content, JSON.stringify({ routes: ["external edit"] }, null, 2));
        });
        it("should allow consecutive saves when timestamp is updated", async () => {
            // 1. First save
            const content1 = JSON.stringify({ routes: ["first save"] }, null, 2);
            const save1Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content1,
                headers: {
                    "content-type": "text/plain",
                },
            });
            assert.strictEqual(save1Response.statusCode, 200);
            const timestamp1 = save1Response.headers["last-modified"];
            assert.ok(timestamp1);
            // Small delay to ensure different timestamp
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 2. Second save with updated timestamp from first save
            const content2 = JSON.stringify({ routes: ["second save"] }, null, 2);
            const save2Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content2,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": timestamp1,
                },
            });
            assert.strictEqual(save2Response.statusCode, 200);
            const timestamp2 = save2Response.headers["last-modified"];
            assert.ok(timestamp2);
            // Small delay to ensure different timestamp
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 3. Third save with updated timestamp from second save
            const content3 = JSON.stringify({ routes: ["third save"] }, null, 2);
            const save3Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content3,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": timestamp2,
                },
            });
            assert.strictEqual(save3Response.statusCode, 200);
            // Verify final content
            const finalContent = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(finalContent, content3);
        });
        it("should handle save-read-save cycle correctly", async () => {
            // 1. Save a file
            const content1 = JSON.stringify({ routes: ["initial"] }, null, 2);
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content1,
                headers: {
                    "content-type": "text/plain",
                },
            });
            assert.strictEqual(saveResponse.statusCode, 200);
            // Small delay to ensure different timestamp
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 2. Read it back to get the new Last-Modified
            const readResponse = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(readResponse.statusCode, 200);
            const lastModified = readResponse.headers["last-modified"];
            assert.ok(lastModified);
            assert.strictEqual(readResponse.body, content1);
            // Small delay to ensure different timestamp
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 3. Save again with the updated timestamp
            const content2 = JSON.stringify({ routes: ["updated"] }, null, 2);
            const save2Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content2,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": lastModified,
                },
            });
            assert.strictEqual(save2Response.statusCode, 200);
            // Verify final content
            const finalContent = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(finalContent, content2);
        });
        it("should detect conflict even with same content but external touch", async () => {
            // 1. Read the file
            const readResponse = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const lastModified = readResponse.headers["last-modified"];
            const originalContent = readResponse.body;
            // Wait to ensure timestamp difference
            await new Promise((resolve) => setTimeout(resolve, 2500));
            // 2. Touch file externally (change timestamp but keep content)
            await writeFile(path.join(testDir, "config", "routes.json"), originalContent, "utf-8");
            // 3. Try to save with same content but old timestamp
            // Should succeed because content is identical
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: originalContent,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": lastModified,
                },
            });
            // Content is same, so no conflict should be detected
            assert.strictEqual(saveResponse.statusCode, 200);
        });
        it("should handle rapid consecutive saves without explicit timestamps", async () => {
            // Multiple rapid saves without providing If-Unmodified-Since
            // Should all succeed as no conflict check is performed
            const saves = [];
            for (let i = 0; i < 5; i++) {
                const content = JSON.stringify({ routes: [`save ${i}`] }, null, 2);
                const savePromise = app.inject({
                    method: "POST",
                    url: "/fs/file?path=/config/routes.json",
                    payload: content,
                    headers: {
                        "content-type": "text/plain",
                    },
                });
                saves.push(savePromise);
            }
            const responses = await Promise.all(saves);
            // All should succeed
            for (const response of responses) {
                assert.strictEqual(response.statusCode, 200);
            }
            // Final content should be from last save (order may vary due to concurrency)
            const finalContent = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.ok(finalContent.includes('"routes"'));
        });
        it("should handle external edit after first save in multi-save sequence", async () => {
            // 1. First save
            const content1 = JSON.stringify({ routes: ["first"] }, null, 2);
            const save1Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content1,
                headers: {
                    "content-type": "text/plain",
                },
            });
            const timestamp1 = save1Response.headers["last-modified"];
            // Wait to ensure timestamp difference
            await new Promise((resolve) => setTimeout(resolve, 2500));
            // 2. External edit (simulating conflict)
            await writeFile(path.join(testDir, "config", "routes.json"), JSON.stringify({ routes: ["external"] }, null, 2), "utf-8");
            // 3. Try second save with timestamp from first save - should fail
            const content2 = JSON.stringify({ routes: ["second"] }, null, 2);
            const save2Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content2,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": timestamp1,
                },
            });
            assert.strictEqual(save2Response.statusCode, 412);
            // 4. Read to get current timestamp
            const readResponse = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const currentTimestamp = readResponse.headers["last-modified"];
            // Small delay
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 5. Try again with current timestamp - should succeed
            const save3Response = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content2,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": currentTimestamp,
                },
            });
            assert.strictEqual(save3Response.statusCode, 200);
            // Verify final content
            const finalContent = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(finalContent, content2);
        });
    });
});
//# sourceMappingURL=fs.spec.js.map