import {
	existsSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { configureLogger, getLogger, resetLoggerConfig } from "../logger.js";

describe("logger", () => {
	let tmpDir: string;

	beforeEach(() => {
		tmpDir = mkdtempSync(join(tmpdir(), "sf-logger-test-"));
		resetLoggerConfig();
	});

	afterEach(() => {
		try {
			rmSync(tmpDir, { recursive: true, force: true });
		} catch {
			/* ignore */
		}
	});

	describe("API exports", () => {
		it("getLogger returns object with info/warn/error/debug methods", () => {
			const log = getLogger("sf.core");
			expect(typeof log.info).toBe("function");
			expect(typeof log.warn).toBe("function");
			expect(typeof log.error).toBe("function");
			expect(typeof log.debug).toBe("function");
		});
	});

	describe("category hierarchy", () => {
		it("filtering sf.uok suppresses sf.uok.loop-adapter", async () => {
			const logs: string[] = [];
			await configureLogger({
				mode: "dev",
				logDir: tmpDir,
				customSink: (record) => {
					logs.push(record.message[0] as string);
				},
				filters: [{ category: ["sf", "uok"], lowestLevel: "fatal" }],
			});
			const parent = getLogger("sf.uok");
			const child = getLogger("sf.uok.loop-adapter");
			parent.info("parent-msg");
			child.info("child-msg");
			expect(logs).not.toContain("parent-msg");
			expect(logs).not.toContain("child-msg");
		});
	});

	describe("PII redaction", () => {
		it("API keys (sk-*) are redacted to [REDACTED]", async () => {
			const logs: string[] = [];
			await configureLogger({
				mode: "dev",
				logDir: tmpDir,
				customSink: (record) => {
					logs.push(JSON.stringify(record.message));
				},
			});
			const log = getLogger("sf.core");
			log.info("key is sk-ant-abc123def456");
			// The customSink receives record.message where pattern redaction
			// has already been applied (via buildRedactingSink).
			expect(
				logs.some((l) => l.includes("[REDACTED]") && !l.includes("sk-ant-")),
			).toBe(true);
		});

		it("home directory paths are redacted to ~", async () => {
			const logs: string[] = [];
			await configureLogger({
				mode: "dev",
				logDir: tmpDir,
				customSink: (record) => {
					logs.push(JSON.stringify(record.message));
				},
			});
			const log = getLogger("sf.core");
			const home = process.env.HOME || "/home/user";
			log.info(`path is ${home}/projects/foo`);
			// The customSink receives record.message where pattern redaction
			// has already been applied (via buildRedactingSink).
			expect(logs.some((l) => l.includes("~/projects/foo"))).toBe(true);
			expect(logs.some((l) => l.includes(home) && !l.includes("~"))).toBe(
				false,
			);
		});
	});

	describe("log level filtering", () => {
		it("warn level suppresses info messages", async () => {
			const logs: string[] = [];
			await configureLogger({
				mode: "dev",
				logDir: tmpDir,
				customSink: (record) => {
					logs.push(record.message[0] as string);
				},
				level: "warning",
			});
			const log = getLogger("sf.core");
			log.info("info-msg");
			log.warn("warn-msg");
			log.error("error-msg");
			expect(logs).not.toContain("info-msg");
			expect(logs).toContain("warn-msg");
			expect(logs).toContain("error-msg");
		});
	});

	describe("dev mode", () => {
		it("output is formatted text not JSON", async () => {
			const lines: string[] = [];
			await configureLogger({
				mode: "dev",
				logDir: tmpDir,
				customSink: (record) => {
					lines.push(record.message[0] as string);
				},
			});
			const log = getLogger("sf.core");
			log.info("hello dev");
			expect(lines.length).toBeGreaterThan(0);
		});

		it("default level is debug so all messages appear", async () => {
			const logs: string[] = [];
			await configureLogger({
				mode: "dev",
				logDir: tmpDir,
				customSink: (record) => {
					logs.push(record.message[0] as string);
				},
			});
			const log = getLogger("sf.core");
			log.debug("debug-msg");
			log.info("info-msg");
			log.warn("warn-msg");
			log.error("error-msg");
			expect(logs).toContain("debug-msg");
			expect(logs).toContain("info-msg");
			expect(logs).toContain("warn-msg");
			expect(logs).toContain("error-msg");
		});
	});

	describe("autonomous mode", () => {
		it("output is valid JSON with timestamp/level/category/message fields", async () => {
			const lines: string[] = [];
			await configureLogger({
				mode: "autonomous",
				logDir: tmpDir,
				customSink: (record) => {
					lines.push(
						JSON.stringify({
							ts: record.timestamp,
							level: record.level,
							category: record.category,
							message: record.message,
						}),
					);
				},
			});
			const log = getLogger("sf.autonomous");
			log.info("hello autonomous");
			expect(lines.length).toBeGreaterThan(0);
			const parsed = JSON.parse(lines[0]);
			expect(parsed.ts).toBeDefined();
			expect(parsed.level).toBe("info");
			expect(parsed.category).toEqual(["sf", "autonomous"]);
			expect(parsed.message).toContain("hello autonomous");
		});

		it("per-session log directory created", async () => {
			const sessionId = "test-session-123";
			const logDir = join(tmpDir, ".sf", "logs");
			await configureLogger({
				mode: "autonomous",
				logDir,
				sessionId,
			});
			const log = getLogger("sf.core");
			log.info("create dir");
			const sessionDir = join(logDir, sessionId);
			expect(existsSync(sessionDir)).toBe(true);
		});

		it("log file contains valid JSONL", async () => {
			const sessionId = "jsonl-session-456";
			const logDir = join(tmpDir, ".sf", "logs");
			await configureLogger({
				mode: "autonomous",
				logDir,
				sessionId,
			});
			const log = getLogger("sf.core");
			log.info("line one");
			log.warn("line two");

			// Flush file sinks via LogTape dispose
			const { dispose } = await import("@logtape/logtape");
			await dispose();
			await new Promise((r) => setTimeout(r, 200));

			const sessionDir = join(logDir, sessionId);
			const allFiles = readdirSync(sessionDir);
			const files = allFiles.filter((f) => f.endsWith(".log"));
			expect(files.length).toBeGreaterThan(0);
			const content = readFileSync(join(sessionDir, files[0]), "utf-8").trim();
			const lines = content.split("\n").filter((l) => l.length > 0);
			expect(lines.length).toBeGreaterThanOrEqual(2);
			for (const line of lines) {
				const parsed = JSON.parse(line);
				expect(parsed["@timestamp"]).toBeDefined();
				expect(parsed.level).toBeDefined();
				expect(parsed.logger).toBeDefined();
				expect(parsed.message).toBeDefined();
			}
		});
	});
});
