import { describe, expect, test } from "bun:test"
import { Hono } from "hono"

import type { UserSessionManager } from "../session/index.ts"
import type { QueryDebugTools, QueryDebugToolDefinition } from "./debug-tools.ts"
import type {
	QueryAgent,
	QueryAgentAsk,
	QueryAgentEventListener,
	QueryAgentStreamEvent,
	QueryAgentEvent,
} from "./query-agent.ts"

import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts"
import { registerAgentHttpHandlers, registerDebugAgentHttpHandlers } from "./http.ts"

const MockUserId = "k7Gx2mPqRvNwYs9TdLfA4bHcJeUo1iZn"

class FakeQueryAgent implements QueryAgent {
	readonly inputs: QueryAgentAsk[] = []
	private readonly events: QueryAgentStreamEvent[]

	constructor(events: QueryAgentStreamEvent[]) {
		this.events = events
	}

	async *ask(input: QueryAgentAsk): AsyncIterable<QueryAgentStreamEvent> {
		this.inputs.push(input)
		for (const event of this.events) {
			yield event
		}
	}

	addEventListener<T extends QueryAgentEvent>(
		_type: T,
		_listener: QueryAgentEventListener<T>,
	): () => void {
		return () => {}
	}

	dispose(): void {}
}

class FakeDebugTools implements QueryDebugTools {
	readonly executions: Array<{ userId: string; toolName: string; params: unknown }> = []
	private readonly tools: QueryDebugToolDefinition[] = [
		{
			name: "freya_test_tool",
			label: "Test Tool",
			description: "A test debug tool.",
			parameters: { query: "string" },
		},
	]

	list(): QueryDebugToolDefinition[] {
		return this.tools
	}

	async execute(userId: string, toolName: string, params: unknown): Promise<unknown> {
		this.executions.push({ userId, toolName, params })
		return { ok: true, userId, toolName, params }
	}
}

function buildTestApp(queryAgent: QueryAgent, userId?: string) {
	const app = new Hono()
	const sessionManager = {
		async getOrCreate() {
			return { agent: queryAgent }
		},
	} as unknown as UserSessionManager

	registerAgentHttpHandlers(app, {
		sessionManager,
		authSessionMiddleware: mockAuthSessionMiddleware(userId),
	})
	return app
}

function buildDebugTestApp(userId: string | undefined, debugTools: QueryDebugTools) {
	const app = new Hono()
	registerDebugAgentHttpHandlers(app, {
		authSessionMiddleware: mockAuthSessionMiddleware(userId),
		debugTools,
	})
	return app
}

describe("POST /api/agent", () => {
	test("returns 401 without auth", async () => {
		const app = buildTestApp(new FakeQueryAgent([]))

		const res = await app.request("/api/agent", {
			method: "POST",
			body: JSON.stringify({ message: "hello" }),
		})

		expect(res.status).toBe(401)
	})

	test("collects text deltas", async () => {
		const agent = new FakeQueryAgent([
			{ type: "text_delta", text: "You should " },
			{ type: "text_delta", text: "leave at 8:30." },
			{ type: "done" },
		])
		const app = buildTestApp(agent, "user-1")

		const res = await app.request("/api/agent", {
			method: "POST",
			body: JSON.stringify({
				message: "What should I do?",
			}),
		})

		expect(res.status).toBe(200)
		expect(agent.inputs).toHaveLength(1)
		expect(agent.inputs[0]!.message).toBe("What should I do?")

		const body = (await res.json()) as {
			message: string
		}
		expect(body.message).toBe("You should leave at 8:30.")
	})

	test("passes conversation id to the query agent", async () => {
		const agent = new FakeQueryAgent([
			{ type: "conversation", conversationId: "conversation-1" },
			{ type: "done" },
		])
		const app = buildTestApp(agent, "user-1")

		const res = await app.request("/api/agent", {
			method: "POST",
			body: JSON.stringify({
				message: "Continue this chat.",
				conversationId: "conversation-1",
			}),
		})

		expect(res.status).toBe(200)
		expect(agent.inputs[0]?.conversationId).toBe("conversation-1")
		const body = (await res.json()) as { conversationId?: string }
		expect(body.conversationId).toBe("conversation-1")
	})

	test("returns 400 for invalid body", async () => {
		const app = buildTestApp(new FakeQueryAgent([]), "user-1")

		const res = await app.request("/api/agent", {
			method: "POST",
			body: JSON.stringify({ feedItemId: "feed-1" }),
		})

		expect(res.status).toBe(400)
	})

	test("returns 400 when body includes feedItemId", async () => {
		const app = buildTestApp(new FakeQueryAgent([]), "user-1")

		const res = await app.request("/api/agent", {
			method: "POST",
			body: JSON.stringify({
				message: "What should I do?",
				feedItemId: "feed-1",
			}),
		})

		expect(res.status).toBe(400)
	})

	test("returns 500 when agent reports an error", async () => {
		const app = buildTestApp(
			new FakeQueryAgent([{ type: "error", message: "model unavailable" }]),
			"user-1",
		)

		const res = await app.request("/api/agent", {
			method: "POST",
			body: JSON.stringify({ message: "hello" }),
		})

		expect(res.status).toBe(500)
		const body = (await res.json()) as { error: string }
		expect(body.error).toBe("model unavailable")
	})
})

describe("query debug tools", () => {
	test("returns 401 without auth", async () => {
		const app = buildDebugTestApp(undefined, new FakeDebugTools())

		const res = await app.request("/api/agent/tools")

		expect(res.status).toBe(401)
	})

	test("lists debug tools", async () => {
		const app = buildDebugTestApp("user-1", new FakeDebugTools())

		const res = await app.request("/api/agent/tools")

		expect(res.status).toBe(200)
		const body = (await res.json()) as { tools: QueryDebugToolDefinition[] }
		expect(body.tools[0]?.name).toBe("freya_test_tool")
	})

	test("executes debug tools for the authenticated user", async () => {
		const debugTools = new FakeDebugTools()
		const app = buildDebugTestApp("user-1", debugTools)

		const res = await app.request("/api/agent/tools/freya_test_tool", {
			method: "POST",
			body: JSON.stringify({ query: "hello" }),
		})

		expect(res.status).toBe(200)
		expect(debugTools.executions).toEqual([
			{
				userId: MockUserId,
				toolName: "freya_test_tool",
				params: { query: "hello" },
			},
		])

		const body = (await res.json()) as { result: unknown }
		expect(body.result).toEqual({
			ok: true,
			userId: MockUserId,
			toolName: "freya_test_tool",
			params: { query: "hello" },
		})
	})

	test("does not register debug tools in production", async () => {
		await withNodeEnv("production", async () => {
			const app = buildDebugTestApp("user-1", new FakeDebugTools())

			const res = await app.request("/api/agent/tools")

			expect(res.status).toBe(404)
		})
	})
})

async function withNodeEnv<T>(nodeEnv: string | undefined, callback: () => Promise<T>): Promise<T> {
	const previous = process.env.NODE_ENV
	if (nodeEnv === undefined) {
		delete process.env.NODE_ENV
	} else {
		process.env.NODE_ENV = nodeEnv
	}

	try {
		return await callback()
	} finally {
		if (previous === undefined) {
			delete process.env.NODE_ENV
		} else {
			process.env.NODE_ENV = previous
		}
	}
}
