import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import {
	closeDatabase,
	getUokMessageBusMetrics,
	getUokMessagesForAgent,
	insertUokMessage,
	openDatabase,
} from "../sf-db.js";
import { AgentInbox, MessageBus } from "../uok/message-bus.js";

const tmpRoots = [];

afterEach(() => {
	closeDatabase();
	for (const dir of tmpRoots.splice(0)) {
		rmSync(dir, { recursive: true, force: true });
	}
});

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-uok-bus-"));
	tmpRoots.push(root);
	return root;
}

// ─── MessageBus ────────────────────────────────────────────────────────────

test("messageBus_send_delivers_message_to_inbox", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	const id = bus.send("agent-a", "agent-b", "hello");
	assert.match(id, /^msg-[0-9a-f-]{36}$/);

	const inbox = bus.getInbox("agent-b");
	const messages = inbox.list();
	assert.equal(messages.length, 1);
	assert.equal(messages[0].from, "agent-a");
	assert.equal(messages[0].body, "hello");
	assert.equal(messages[0].read, false);
});

test("messageBus_broadcast_delivers_to_all_recipients", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	const ids = bus.broadcast("agent-a", ["agent-b", "agent-c"], "alert");
	assert.equal(ids.length, 2);
	assert.equal(bus.getInbox("agent-b").unreadCount, 1);
	assert.equal(bus.getInbox("agent-c").unreadCount, 1);
});

test("messageBus_persists_across_reconstruction", () => {
	const root = makeProject();
	const bus1 = new MessageBus(root);
	bus1.send("agent-a", "agent-b", "persistent");

	const bus2 = new MessageBus(root);
	const inbox = bus2.getInbox("agent-b");
	const messages = inbox.list();
	assert.equal(messages.length, 1);
	assert.equal(messages[0].body, "persistent");
});

test("messageBus_send_when_inbox_already_hydrated_does_not_double_insert", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	const inbox = bus.getInbox("agent-b");

	const id = bus.send("agent-a", "agent-b", "after-hydrate");

	assert.equal(inbox.list().length, 1);
	assert.equal(inbox.list()[0].id, id);
	assert.equal(inbox.list()[0].body, "after-hydrate");
	const bus2 = new MessageBus(root);
	assert.equal(bus2.getInbox("agent-b").list().length, 1);
});

test("messageBus_sendOnce_when_called_repeatedly_keeps_single_inbox_message", () => {
	const root = makeProject();
	const bus = new MessageBus(root);

	const first = bus.sendOnce(
		"uok-diagnostics",
		"uok-observer",
		"same health issue",
		{ verdict: "attention" },
		"diagnostic:stable",
	);
	const second = bus.sendOnce(
		"uok-diagnostics",
		"uok-observer",
		"same health issue",
		{ verdict: "attention" },
		"diagnostic:stable",
	);

	assert.equal(second, first);
	const messages = bus.getInbox("uok-observer").list();
	assert.equal(messages.length, 1);
	assert.equal(messages[0].body, "same health issue");
	assert.equal(messages[0].metadata.dedupeKey, "diagnostic:stable");
});

test("messageBus_markRead_updates_state_and_persists", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	bus.send("agent-a", "agent-b", "read-me");
	const inbox = bus.getInbox("agent-b");
	const msg = inbox.list()[0];
	assert.equal(inbox.unreadCount, 1);

	inbox.markRead(msg.id);
	assert.equal(inbox.unreadCount, 0);

	// Reconstruct and verify read state persisted
	const bus2 = new MessageBus(root);
	const inbox2 = bus2.getInbox("agent-b");
	assert.equal(inbox2.list(true).length, 0);
});

test("messageBus_compact_removes_old_messages", () => {
	const root = makeProject();
	const bus = new MessageBus(root, { retentionDays: 1 });
	// Insert an old message directly into the DB
	const oldMsg = {
		id: "msg-old",
		from: "agent-a",
		to: "agent-b",
		body: "old",
		sentAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
		deliveredAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
		metadata: {},
	};
	insertUokMessage(oldMsg);

	const result = bus.compact();
	assert.equal(result.after, 0);
	assert.ok(result.before >= 1);

	const inbox = bus.getInbox("agent-b");
	inbox.refresh();
	assert.equal(inbox.list().length, 0);
});

test("messageBus_send_when_threshold_reached_auto_compacts_old_messages", () => {
	const root = makeProject();
	const bus = new MessageBus(root, {
		retentionDays: 1,
		autoCompactThreshold: 1,
	});
	insertUokMessage({
		id: "msg-old-auto",
		from: "agent-a",
		to: "agent-b",
		body: "old",
		sentAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
		deliveredAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
		metadata: {},
	});

	bus.send("agent-a", "agent-b", "fresh");

	const inbox = bus.getInbox("agent-b");
	inbox.refresh();
	const bodies = inbox.list().map((m) => m.body);
	assert.deepEqual(bodies, ["fresh"]);
});

test("messageBus_getConversation_filters_by_pair", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	bus.send("agent-a", "agent-b", "ab1");
	bus.send("agent-b", "agent-a", "ba1");
	bus.send("agent-c", "agent-b", "cb1");

	const conv = bus.getConversation("agent-a", "agent-b");
	assert.equal(conv.length, 2);
	assert.ok(conv.every((m) => m.body !== "cb1"));
});

test("messageBus_getReplyTo_finds_latest_reply_without_inbox_refresh", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	const requestId = bus.send("agent:coordinator", "agent:worker-1", "work");
	bus.send("agent:worker-1", "agent:coordinator", "older", {
		replyTo: requestId,
	});
	const latestId = bus.send("agent:worker-1", "agent:coordinator", "latest", {
		replyTo: requestId,
	});
	bus.send("agent:worker-2", "agent:coordinator", "wrong-agent", {
		replyTo: requestId,
	});

	const reply = bus.getReplyTo(requestId, "agent:worker-1");

	assert.equal(reply.id, latestId);
	assert.equal(reply.from, "agent:worker-1");
	assert.equal(reply.to, "agent:coordinator");
	assert.equal(reply.body, "latest");
	assert.equal(reply.metadata.replyTo, requestId);
});

test("messageBus_inbox_enforces_maxSize", () => {
	const root = makeProject();
	const bus = new MessageBus(root, { maxInboxSize: 3 });
	for (let i = 0; i < 5; i++) {
		bus.send("agent-a", "agent-b", `msg-${i}`);
	}
	const inbox = bus.getInbox("agent-b");
	assert.equal(inbox.list().length, 3);
	assert.equal(inbox.list()[0].body, "msg-2");
});

// ─── AgentInbox ────────────────────────────────────────────────────────────

test("agentInbox_list_unreadOnly_filters_correctly", () => {
	const root = makeProject();
	const inbox = new AgentInbox("agent-b", root);
	inbox.receive({ id: "1", from: "a", body: "x" });
	inbox.receive({ id: "2", from: "a", body: "y" });
	inbox.markRead("1");

	assert.equal(inbox.list().length, 2);
	assert.equal(inbox.list(true).length, 1);
	assert.equal(inbox.list(true)[0].id, "2");
});

test("agentInbox_markRead_returns_false_for_unknown_id", () => {
	const root = makeProject();
	const inbox = new AgentInbox("agent-b", root);
	assert.equal(inbox.markRead("nope"), false);
});

test("getUokMessageBusMetrics_returns_zero_when_empty", () => {
	makeProject();
	const m = getUokMessageBusMetrics();
	assert.equal(m.totalMessages, 0);
	assert.equal(m.totalUnread, 0);
	assert.equal(m.uniqueAgents, 0);
	assert.equal(m.uniqueConversations, 0);
});

test("getUokMessageBusMetrics_counts_messages_and_unread", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	bus.send("agent-a", "agent-b", "hello");
	bus.send("agent-a", "agent-c", "world");
	bus.send("agent-c", "agent-b", "unread");

	const m = getUokMessageBusMetrics();
	assert.equal(m.totalMessages, 3);
	assert.equal(m.totalUnread, 3);
	assert.equal(m.uniqueAgents, 2); // agent-b, agent-c
	assert.equal(m.uniqueConversations, 3);

	// Mark one read
	bus.getInbox("agent-b").markRead(bus.getInbox("agent-b").list()[0].id);
	const m2 = getUokMessageBusMetrics();
	assert.equal(m2.totalUnread, 2);
});

test("messageBus_autoCompact_triggers_when_threshold_exceeded", () => {
	const root = makeProject();
	const bus = new MessageBus(root, {
		autoCompactThreshold: 5,
		retentionDays: 1,
	});
	// Insert old messages directly to bypass auto-compact on send
	for (let i = 0; i < 4; i++) {
		insertUokMessage({
			id: `msg-old-${i}`,
			from: "agent-a",
			to: "agent-b",
			body: `old-${i}`,
			sentAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
			metadata: {},
		});
	}
	let m = getUokMessageBusMetrics();
	assert.equal(m.totalMessages, 4);

	// Sending one more message should trigger auto-compact (5 >= threshold)
	bus.send("agent-a", "agent-b", "new");
	m = getUokMessageBusMetrics();
	assert.equal(m.totalMessages, 1); // old messages compacted, only new remains
});

test("getUokMessageBusMetrics_ignores_reads_by_non_recipient_for_unread_count", () => {
	const root = makeProject();
	const bus = new MessageBus(root);
	const id = bus.send("agent-a", "agent-b", "hello");

	bus.getInbox("agent-c").markRead(id);
	const m = getUokMessageBusMetrics();
	assert.equal(m.totalMessages, 1);
	assert.equal(m.totalUnread, 1);
});

test("messageBus_send_when_switching_projects_uses_current_project_db", () => {
	const first = makeProject();
	const second = makeProject();

	new MessageBus(first).send("agent-a", "agent-b", "first");
	new MessageBus(second).send("agent-a", "agent-b", "second");

	closeDatabase();
	openDatabase(join(first, ".sf", "sf.db"));
	assert.deepEqual(
		getUokMessagesForAgent("agent-b").map((message) => message.body),
		["first"],
	);
	closeDatabase();
	openDatabase(join(second, ".sf", "sf.db"));
	assert.deepEqual(
		getUokMessagesForAgent("agent-b").map((message) => message.body),
		["second"],
	);
});
