import type { AgentEvent, ConversationMeta } from "@dispatch/transport-contract";
import type { StepId } from "@dispatch/wire";
import { describe, expect, it } from "vitest";
import {
	extractLastText,
	formatConversationList,
	formatRelativeTime,
	renderEvent,
} from "./render.js";

describe("renderEvent", () => {
	const opts = { showReasoning: false };
	const optsReasoning = { showReasoning: true };

	it("renders text-delta as stdout", () => {
		const e: AgentEvent = {
			type: "text-delta",
			conversationId: "c",
			turnId: "t",
			delta: "hello",
		};
		expect(renderEvent(e, opts)).toEqual({ stdout: "hello" });
	});

	it("hides reasoning-delta by default", () => {
		const e: AgentEvent = {
			type: "reasoning-delta",
			conversationId: "c",
			turnId: "t",
			delta: "thinking...",
		};
		expect(renderEvent(e, opts)).toBeUndefined();
	});

	it("shows reasoning-delta when showReasoning is true", () => {
		const e: AgentEvent = {
			type: "reasoning-delta",
			conversationId: "c",
			turnId: "t",
			delta: "thinking...",
		};
		expect(renderEvent(e, optsReasoning)).toEqual({ stdout: "thinking..." });
	});

	it("renders tool-call with name and JSON input", () => {
		const e: AgentEvent = {
			type: "tool-call",
			conversationId: "c",
			turnId: "t",
			stepId: "t1#0" as StepId,
			toolCallId: "tc1",
			toolName: "read_file",
			input: { path: "/foo" },
		};
		const result = renderEvent(e, opts);
		expect(result?.stdout).toContain("[tool] read_file");
		expect(result?.stdout).toContain('"/foo"');
	});

	it("renders tool-output data as stdout", () => {
		const e: AgentEvent = {
			type: "tool-output",
			conversationId: "c",
			turnId: "t",
			toolCallId: "tc1",
			data: "some output",
			stream: "stdout",
		};
		expect(renderEvent(e, opts)).toEqual({ stdout: "some output" });
	});

	it("renders tool-result without error", () => {
		const e: AgentEvent = {
			type: "tool-result",
			conversationId: "c",
			turnId: "t",
			stepId: "t1#0" as StepId,
			toolCallId: "tc1",
			toolName: "read_file",
			content: "file contents",
			isError: false,
		};
		expect(renderEvent(e, opts)).toEqual({
			stdout: "[tool:read_file] file contents\n",
		});
	});

	it("renders tool-result with error flag", () => {
		const e: AgentEvent = {
			type: "tool-result",
			conversationId: "c",
			turnId: "t",
			stepId: "t1#0" as StepId,
			toolCallId: "tc1",
			toolName: "read_file",
			content: "not found",
			isError: true,
		};
		expect(renderEvent(e, opts)).toEqual({
			stdout: "[tool:read_file] ERROR not found\n",
		});
	});

	it("renders usage event", () => {
		const e: AgentEvent = {
			type: "usage",
			conversationId: "c",
			turnId: "t",
			usage: { inputTokens: 100, outputTokens: 50 },
		};
		expect(renderEvent(e, opts)).toEqual({
			stdout: "\n[usage] in=100 out=50\n",
		});
	});

	it("renders error event to stderr", () => {
		const e: AgentEvent = {
			type: "error",
			conversationId: "c",
			turnId: "t",
			message: "something went wrong",
		};
		expect(renderEvent(e, opts)).toEqual({
			stderr: "[error] something went wrong\n",
		});
	});

	it("returns undefined for status", () => {
		const e: AgentEvent = {
			type: "status",
			conversationId: "c",
			status: "running",
		};
		expect(renderEvent(e, opts)).toBeUndefined();
	});

	it("returns undefined for turn-start", () => {
		const e: AgentEvent = { type: "turn-start", conversationId: "c", turnId: "t" };
		expect(renderEvent(e, opts)).toBeUndefined();
	});

	it("returns undefined for turn-sealed", () => {
		const e: AgentEvent = { type: "turn-sealed", conversationId: "c", turnId: "t" };
		expect(renderEvent(e, opts)).toBeUndefined();
	});

	it("returns undefined for done", () => {
		const e: AgentEvent = {
			type: "done",
			conversationId: "c",
			turnId: "t",
			reason: "completed",
		};
		expect(renderEvent(e, opts)).toBeUndefined();
	});
});

describe("extractLastText", () => {
	it("accumulates text deltas", () => {
		const events: AgentEvent[] = [
			{ type: "text-delta", conversationId: "c", turnId: "t", delta: "Hello" },
			{ type: "text-delta", conversationId: "c", turnId: "t", delta: ", " },
			{ type: "text-delta", conversationId: "c", turnId: "t", delta: "world" },
			{ type: "done", conversationId: "c", turnId: "t", reason: "completed" },
		];
		expect(extractLastText(events)).toBe("Hello, world");
	});

	it("returns empty string when no text-delta events", () => {
		const events: AgentEvent[] = [
			{ type: "turn-start", conversationId: "c", turnId: "t" },
			{ type: "done", conversationId: "c", turnId: "t", reason: "completed" },
		];
		expect(extractLastText(events)).toBe("");
	});

	it("ignores non-text-delta events but keeps deltas interleaved among them", () => {
		const events: AgentEvent[] = [
			{ type: "text-delta", conversationId: "c", turnId: "t", delta: "a" },
			{
				type: "tool-call",
				conversationId: "c",
				turnId: "t",
				stepId: "t1#0" as StepId,
				toolCallId: "tc1",
				toolName: "read_file",
				input: {},
			},
			{ type: "text-delta", conversationId: "c", turnId: "t", delta: "b" },
		];
		expect(extractLastText(events)).toBe("ab");
	});

	it("returns empty string for an empty event list", () => {
		expect(extractLastText([])).toBe("");
	});
});

describe("formatRelativeTime", () => {
	const now = 1_000_000_000_000; // fixed clock

	it("returns 'just now' for under a minute", () => {
		expect(formatRelativeTime(now - 30_000, now)).toBe("just now");
	});

	it("returns minutes ago", () => {
		expect(formatRelativeTime(now - 5 * 60_000, now)).toBe("5m ago");
	});

	it("returns hours ago", () => {
		expect(formatRelativeTime(now - 3 * 3_600_000, now)).toBe("3h ago");
	});

	it("returns days ago", () => {
		expect(formatRelativeTime(now - 2 * 86_400_000, now)).toBe("2d ago");
	});

	it("returns a date past a week", () => {
		// 2001-09-09T01:46:40.000Z
		expect(formatRelativeTime(now - 8 * 86_400_000, now)).toBe("2001-09-01");
	});
});

describe("formatConversationList", () => {
	const now = 1_000_000_000_000;

	const conv = (id: string, title: string, ageMs: number): ConversationMeta => ({
		id,
		title,
		createdAt: now - ageMs - 1000,
		lastActivityAt: now - ageMs,
	});

	it("returns empty string for an empty list", () => {
		expect(formatConversationList([], now)).toBe("");
	});

	it("formats one row with short id, title, relative time", () => {
		const list = [conv("abcdef1234567890", "hello world", 5 * 60_000)];
		expect(formatConversationList(list, now)).toBe("abcdef12 | hello world | 5m ago");
	});

	it("formats multiple rows one per line", () => {
		const list = [
			conv("abcdef1234567890", "first", 5 * 60_000),
			conv("0123456789abcdef", "second", 3 * 3_600_000),
		];
		expect(formatConversationList(list, now)).toBe(
			"abcdef12 | first | 5m ago\n01234567 | second | 3h ago",
		);
	});
});
