import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, test, vi } from "vitest";
import { HaltWatchdog } from "../auto/loop.js";
import { installNotifyInterceptor } from "../bootstrap/notify-interceptor.js";
import {
	_resetNotificationStore,
	appendNotification,
	getAppendFailureCount,
	getLastAppendFailure,
	initNotificationStore,
	NOTICE_KIND,
} from "../notification-store.js";

let testDir;

beforeEach(() => {
	_resetNotificationStore();
	testDir = join(tmpdir(), `sf-watchdog-test-${Date.now()}`);
	mkdirSync(testDir, { recursive: true });
	writeFileSync(
		join(testDir, "package.json"),
		JSON.stringify({ name: "singularity-forge" }),
	);
	initNotificationStore(testDir);
});

afterEach(() => {
	rmSync(testDir, { recursive: true, force: true });
	_resetNotificationStore();
	vi.useRealTimers();
});

function readNotifications() {
	const notificationFile = join(testDir, ".sf", "notifications.jsonl");
	if (!existsSync(notificationFile)) return [];
	const content = readFileSync(notificationFile, "utf-8").trim();
	if (!content) return [];
	return content.split("\n").map((line) => JSON.parse(line));
}

test("HaltWatchdog.check()_when_idle_exceeds_threshold_emits_BLOCKING_NOTICE_to_jsonl", () => {
	vi.useFakeTimers();
	const thresholdMs = 10;
	const watchdog = new HaltWatchdog(testDir, thresholdMs);
	const originalNotify = vi.fn();
	const mockCtx = { ui: { notify: originalNotify }, basePath: testDir };
	installNotifyInterceptor(mockCtx);

	watchdog.heartbeat();
	vi.advanceTimersByTime(thresholdMs + 5);
	const result = watchdog.check(mockCtx, 1);

	assert.equal(result.stuck, true);
	assert.equal(originalNotify.mock.calls.length, 1);
	const entries = readNotifications();
	assert.equal(entries.length, 1);
	assert.equal(entries[0].noticeKind, NOTICE_KIND.BLOCKING_NOTICE);
	assert.equal(entries[0].metadata.dedupe_key, "halt-watchdog-stuck");
	assert.match(entries[0].message, /idle for/);
});

test("HaltWatchdog.check()_when_within_threshold_emits_no_notification", () => {
	vi.useFakeTimers();
	const thresholdMs = 10_000;
	const watchdog = new HaltWatchdog(testDir, thresholdMs);
	const originalNotify = vi.fn();
	const mockCtx = { ui: { notify: originalNotify } };
	installNotifyInterceptor(mockCtx);

	watchdog.heartbeat();
	vi.advanceTimersByTime(100);
	const result = watchdog.check(mockCtx, 1);

	assert.equal(result.stuck, false);
	assert.equal(originalNotify.mock.calls.length, 0);
	assert.deepEqual(readNotifications(), []);
});

test("HaltWatchdog.check()_reports_only_once_per_idle_period", () => {
	vi.useFakeTimers();
	const thresholdMs = 10;
	const watchdog = new HaltWatchdog(testDir, thresholdMs);
	const originalNotify = vi.fn();
	const mockCtx = { ui: { notify: originalNotify }, basePath: testDir };
	installNotifyInterceptor(mockCtx);

	watchdog.heartbeat();
	vi.advanceTimersByTime(thresholdMs + 5);
	watchdog.check(mockCtx, 1);
	watchdog.check(mockCtx, 2);

	assert.equal(readNotifications().length, 1);
});

test("appendNotification_fails_open_when_basePath_is_null", () => {
	_resetNotificationStore();
	// basePath is null because initNotificationStore was never called after reset

	assert.doesNotThrow(() => {
		appendNotification("test", "info", "test");
	});

	// When basePath is null, appendNotification returns early without error.
	// The fail-open contract is: no crash, no unhandled exception.
	assert.equal(getAppendFailureCount(), 0);
	assert.equal(getLastAppendFailure(), null);
});
