// 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 Race Conditions", () => {
    let app;
    let testDir;
    beforeEach(async () => {
        testDir = await createTempTestDir("fs-race-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("Save with rapid successive reads", () => {
        it("should handle save followed immediately by read", async () => {
            // Simulate the client save workflow:
            // 1. Client reads file to get initial timestamp
            const readResponse1 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(readResponse1.statusCode, 200);
            const timestamp1 = readResponse1.headers["last-modified"];
            assert.ok(timestamp1);
            // Small delay to ensure different timestamp
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 2. Client saves the file
            const newContent = JSON.stringify({ routes: ["new 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": timestamp1,
                },
            });
            assert.strictEqual(saveResponse.statusCode, 200);
            const timestamp2 = saveResponse.headers["last-modified"];
            assert.ok(timestamp2);
            assert.notStrictEqual(timestamp2, timestamp1);
            // 3. Immediately after save, simulate FileChangeMonitor reading the file
            // This happens when SSE event arrives
            const readResponse2 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(readResponse2.statusCode, 200);
            const timestamp3 = readResponse2.headers["last-modified"];
            // Verify the content is what we just saved
            assert.strictEqual(readResponse2.body, newContent);
            // Timestamp should match what the save returned
            assert.strictEqual(timestamp3, timestamp2);
        });
        it("should handle save without lastModified followed by read", async () => {
            // Simulate a NEW file being created (no lastModified)
            const newContent = JSON.stringify({ routes: ["first save"] }, null, 2);
            // 1. Save without if-unmodified-since header (new file)
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/new-file.json",
                payload: newContent,
                headers: {
                    "content-type": "text/plain",
                },
            });
            assert.strictEqual(saveResponse.statusCode, 200);
            const timestamp1 = saveResponse.headers["last-modified"];
            assert.ok(timestamp1);
            // 2. Read the file immediately after
            const readResponse = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/new-file.json",
            });
            assert.strictEqual(readResponse.statusCode, 200);
            assert.strictEqual(readResponse.body, newContent);
            // Timestamp should match
            assert.strictEqual(readResponse.headers["last-modified"], timestamp1);
        });
        it("should handle save with stale timestamp causing 412 error", async () => {
            // 1. Read file to get timestamp
            const read1 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const timestamp1 = read1.headers["last-modified"];
            // 2. External modification (simulate another process editing the file)
            await new Promise((resolve) => setTimeout(resolve, 2500));
            await writeFile(path.join(testDir, "config", "routes.json"), JSON.stringify({ routes: ["external edit"] }, null, 2), "utf-8");
            // 3. Try to save with stale timestamp - should fail with 412
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: JSON.stringify({ routes: ["my edit"] }, null, 2),
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": timestamp1,
                },
            });
            assert.strictEqual(saveResponse.statusCode, 412);
            // 4. Read file again to get current state
            const read2 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const timestamp2 = read2.headers["last-modified"];
            // Timestamp should be different from original
            assert.notStrictEqual(timestamp2, timestamp1);
            // Content should be the external edit, not our attempted save
            assert.strictEqual(read2.body, JSON.stringify({ routes: ["external edit"] }, null, 2));
        });
        it("should handle rapid save-read-save cycle", async () => {
            // This simulates the client workflow with FileChangeMonitor
            // 1. Initial read
            const read1 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const timestamp1 = read1.headers["last-modified"];
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 2. First save
            const content1 = JSON.stringify({ routes: ["save 1"] }, null, 2);
            const save1 = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content1,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": timestamp1,
                },
            });
            assert.strictEqual(save1.statusCode, 200);
            const timestamp2 = save1.headers["last-modified"];
            // 3. FileChangeMonitor reads (SSE event)
            const read2 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(read2.body, content1);
            const timestamp2b = read2.headers["last-modified"];
            assert.strictEqual(timestamp2b, timestamp2);
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 4. Second save with updated timestamp from first save
            const content2 = JSON.stringify({ routes: ["save 2"] }, null, 2);
            const save2 = await app.inject({
                method: "POST",
                url: "/fs/file?path=/config/routes.json",
                payload: content2,
                headers: {
                    "content-type": "text/plain",
                    "if-unmodified-since": timestamp2,
                },
            });
            assert.strictEqual(save2.statusCode, 200);
            const timestamp3 = save2.headers["last-modified"];
            // 5. FileChangeMonitor reads again
            const read3 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(read3.body, content2);
            assert.strictEqual(read3.headers["last-modified"], timestamp3);
            // Verify final content on disk
            const finalContent = await readFile(path.join(testDir, "config", "routes.json"), "utf-8");
            assert.strictEqual(finalContent, content2);
        });
    });
    describe("Timestamp precision and comparison", () => {
        it("should handle timestamp comparison with second precision", async () => {
            // HTTP Last-Modified headers have second precision, not millisecond
            // 1. Get initial timestamp
            const read1 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const httpTimestamp = read1.headers["last-modified"];
            const timestamp1 = new Date(httpTimestamp).getTime();
            // 2. Get actual file timestamp
            const stats = await stat(path.join(testDir, "config", "routes.json"));
            const actualTimestamp = stats.mtime.getTime();
            // Timestamps should match when rounded to seconds
            assert.strictEqual(Math.floor(timestamp1 / 1000), Math.floor(actualTimestamp / 1000));
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 3. Save with the HTTP timestamp
            const newContent = JSON.stringify({ routes: ["test"] }, 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": httpTimestamp,
                },
            });
            assert.strictEqual(saveResponse.statusCode, 200);
        });
    });
    describe("Multiple files save race conditions", () => {
        it("should handle multipart save with immediate reads", async () => {
            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`;
                    payload += `\r\n`;
                    payload += `${file.content}\r\n`;
                }
                payload += `--${boundary}--\r\n`;
                return { payload, boundary };
            }
            // 1. Read initial timestamps for multiple files
            const read1 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            const timestamp1 = read1.headers["last-modified"];
            const read2 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/modules/my-handler.ts",
            });
            const timestamp2 = read2.headers["last-modified"];
            await new Promise((resolve) => setTimeout(resolve, 1100));
            // 2. Save multiple files at once
            const newContent1 = JSON.stringify({ routes: ["multi save"] }, null, 2);
            const newContent2 = "// Multi save test\nexport default async () => {};";
            const { payload, boundary } = createMultipartPayload([
                { filename: "/config/routes.json", content: newContent1 },
                { filename: "/modules/my-handler.ts", content: newContent2 },
            ]);
            const headerKey1 = `x-if-unmodified-since-${encodeURIComponent("/config/routes.json")}`;
            const headerKey2 = `x-if-unmodified-since-${encodeURIComponent("/modules/my-handler.ts")}`;
            const saveResponse = await app.inject({
                method: "POST",
                url: "/fs/files",
                payload,
                headers: {
                    "content-type": `multipart/form-data; boundary=${boundary}`,
                    [headerKey1]: timestamp1,
                    [headerKey2]: timestamp2,
                },
            });
            assert.strictEqual(saveResponse.statusCode, 200);
            const responseBody = JSON.parse(saveResponse.body);
            assert.ok(responseBody.files);
            assert.ok(responseBody.files["/config/routes.json"]);
            assert.ok(responseBody.files["/modules/my-handler.ts"]);
            // 3. Immediately read both files (simulating FileChangeMonitor)
            const readAfter1 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/config/routes.json",
            });
            assert.strictEqual(readAfter1.body, newContent1);
            const readAfter2 = await app.inject({
                method: "GET",
                url: "/fs/file?path=/modules/my-handler.ts",
            });
            assert.strictEqual(readAfter2.body, newContent2);
            // Timestamps should match response
            const timestamp1New = new Date(responseBody.files["/config/routes.json"]).toUTCString();
            const timestamp2New = new Date(responseBody.files["/modules/my-handler.ts"]).toUTCString();
            assert.strictEqual(readAfter1.headers["last-modified"], timestamp1New);
            assert.strictEqual(readAfter2.headers["last-modified"], timestamp2New);
        });
    });
});
//# sourceMappingURL=fs-race-condition.spec.js.map