import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	rmSync,
	symlinkSync,
	utimesSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { pruneStaleTraces } from "../uok/trace-writer.js";

let tempDirs = [];

function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-trace-janitor-"));
	tempDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	return dir;
}

afterEach(() => {
	for (const dir of tempDirs) {
		rmSync(dir, { recursive: true, force: true });
	}
	tempDirs = [];
});

function makeTraceFile(project, name, daysOld) {
	const tracesDir = join(project, ".sf", "traces");
	mkdirSync(tracesDir, { recursive: true });
	const path = join(tracesDir, name);
	writeFileSync(path, '{"ts":"2024-01-01T00:00:00Z","type":"gate_run"}\n');
	if (typeof daysOld === "number") {
		const epoch = (Date.now() - daysOld * 24 * 60 * 60 * 1000) / 1000;
		utimesSync(path, epoch, epoch);
	}
	return path;
}

describe("pruneStaleTraces", () => {
	test("removes jsonl files older than retention window", () => {
		const project = makeProject();
		const oldFile = makeTraceFile(project, "pre-dispatch:old.jsonl", 45);
		const freshFile = makeTraceFile(project, "pre-dispatch:fresh.jsonl", 5);
		expect(existsSync(oldFile)).toBe(true);
		expect(existsSync(freshFile)).toBe(true);

		const result = pruneStaleTraces(project);
		expect(result.pruned).toBe(1);
		expect(existsSync(oldFile)).toBe(false);
		expect(existsSync(freshFile)).toBe(true);
	});

	test("respects a custom retention window", () => {
		const project = makeProject();
		const file = makeTraceFile(project, "pre-dispatch:tenday.jsonl", 10);
		const result = pruneStaleTraces(project, { retentionDays: 7 });
		expect(result.pruned).toBe(1);
		expect(existsSync(file)).toBe(false);
	});

	test("never touches the `latest` symlink", () => {
		const project = makeProject();
		makeTraceFile(project, "pre-dispatch:current.jsonl", 0);
		const latest = join(project, ".sf", "traces", "latest");
		symlinkSync("pre-dispatch:current.jsonl", latest);
		// Make `latest` look old via its target; the symlink itself is fine.
		pruneStaleTraces(project);
		expect(existsSync(latest)).toBe(true);
	});

	test("ignores non-jsonl files", () => {
		const project = makeProject();
		const tracesDir = join(project, ".sf", "traces");
		mkdirSync(tracesDir, { recursive: true });
		const txt = join(tracesDir, "notes.txt");
		writeFileSync(txt, "ignored");
		const epoch = (Date.now() - 90 * 24 * 60 * 60 * 1000) / 1000;
		utimesSync(txt, epoch, epoch);
		pruneStaleTraces(project);
		expect(existsSync(txt)).toBe(true);
	});

	test("returns zero-counts when traces dir does not exist", () => {
		const project = makeProject();
		// no traces dir
		const result = pruneStaleTraces(project);
		expect(result).toEqual({ scanned: 0, pruned: 0, errors: 0 });
	});

	test("respects maxDeletePerCall safety cap", () => {
		const project = makeProject();
		for (let i = 0; i < 5; i++) {
			makeTraceFile(project, `pre-dispatch:old-${i}.jsonl`, 60);
		}
		const result = pruneStaleTraces(project, { maxDeletePerCall: 2 });
		expect(result.pruned).toBe(2);
	});

	test("does not throw on missing basePath", () => {
		expect(() => pruneStaleTraces("")).not.toThrow();
		expect(() => pruneStaleTraces(undefined)).not.toThrow();
	});
});
