import { describe, expect, it } from "bun:test";
import { encodeResponse, encodeStream, parseRequest } from "../src/providers/anthropic-messages-server";
import type { AssistantMessage, AssistantMessageEvent, ToolResultMessage } from "../src/types";
import { AssistantMessageEventStream } from "../src/utils/event-stream";

function emptyUsage(): AssistantMessage["usage"] {
	return {
		input: 0,
		output: 0,
		cacheRead: 0,
		cacheWrite: 0,
		totalTokens: 0,
		cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
	};
}

function makeStream(events: AssistantMessageEvent[]): AssistantMessageEventStream {
	const s = new AssistantMessageEventStream();
	queueMicrotask(() => {
		for (const ev of events) s.push(ev);
		s.end();
	});
	return s;
}

interface SseEvent {
	event: string;
	data: Record<string, unknown>;
}

async function collectSse(stream: ReadableStream<Uint8Array>): Promise<SseEvent[]> {
	const reader = stream.getReader();
	const decoder = new TextDecoder();
	let buf = "";
	const out: SseEvent[] = [];
	while (true) {
		const { value, done } = await reader.read();
		if (done) break;
		buf += decoder.decode(value, { stream: true });
	}
	buf += decoder.decode();
	for (const chunk of buf.split("\n\n")) {
		if (!chunk.trim()) continue;
		let event = "";
		let dataLine = "";
		for (const line of chunk.split("\n")) {
			if (line.startsWith("event: ")) event = line.slice(7);
			else if (line.startsWith("data: ")) dataLine = line.slice(6);
		}
		out.push({ event, data: JSON.parse(dataLine) as Record<string, unknown> });
	}
	return out;
}

describe("anthropic-messages parseRequest", () => {
	it("parses system + user + assistant(thinking,text,tool_use) + tool_result", () => {
		const parsed = parseRequest({
			model: "claude-opus-4-7",
			max_tokens: 1024,
			temperature: 0.2,
			top_p: 0.9,
			stop_sequences: ["\n\n"],
			tool_choice: { type: "any" },
			thinking: { type: "enabled", budget_tokens: 2048 },
			system: [
				{ type: "text", text: "You are X" },
				{ type: "text", text: "Be brief." },
			],
			tools: [
				{
					name: "lookup",
					description: "find a thing",
					input_schema: { type: "object", properties: { q: { type: "string" } }, required: ["q"] },
				},
			],
			messages: [
				{ role: "user", content: "hi" },
				{
					role: "assistant",
					content: [
						{ type: "thinking", thinking: "hmm", signature: "sig-1" },
						{ type: "redacted_thinking", data: "REDACTED" },
						{ type: "text", text: "calling tool" },
						{ type: "tool_use", id: "toolu_abc", name: "lookup", input: { q: "x" } },
					],
				},
				{
					role: "user",
					content: [
						{
							type: "tool_result",
							tool_use_id: "toolu_abc",
							content: [{ type: "text", text: "result text" }],
							is_error: false,
						},
					],
				},
				{
					role: "user",
					content: [
						{
							type: "tool_result",
							tool_use_id: "toolu_def",
							content: "string body",
							is_error: true,
						},
						{ type: "text", text: "and another result coming" },
					],
				},
			],
		});

		expect(parsed.modelId).toBe("claude-opus-4-7");
		expect(parsed.stream).toBe(false);
		expect(parsed.context.systemPrompt).toEqual(["You are X\n\nBe brief."]);
		expect(parsed.options.maxOutputTokens).toBe(1024);
		expect(parsed.options.temperature).toBe(0.2);
		expect(parsed.options.topP).toBe(0.9);
		expect(parsed.options.stopSequences).toEqual(["\n\n"]);
		expect(parsed.options.toolChoice).toBe("required");
		expect(parsed.options.explicitThinkingBudgetTokens).toBe(2048);
		expect(parsed.options.extra).toBeUndefined();

		expect(parsed.context.tools).toHaveLength(1);
		const tool = parsed.context.tools![0]!;
		expect(tool.name).toBe("lookup");
		expect(tool.description).toBe("find a thing");
		expect(tool.parameters).toEqual({
			type: "object",
			properties: { q: { type: "string" } },
			required: ["q"],
		});

		// messages: user("hi"), assistant(4 blocks), toolResult(toolu_abc),
		// toolResult(toolu_def), user("and another result coming")
		const msgs = parsed.context.messages;
		expect(msgs).toHaveLength(5);

		expect(msgs[0]).toMatchObject({ role: "user", content: "hi" });

		const asst = msgs[1];
		expect(asst.role).toBe("assistant");
		if (asst.role !== "assistant") throw new Error();
		expect(asst.content).toEqual([
			{ type: "thinking", thinking: "hmm", thinkingSignature: "sig-1" },
			{ type: "redactedThinking", data: "REDACTED" },
			{ type: "text", text: "calling tool" },
			{ type: "toolCall", id: "toolu_abc", name: "lookup", arguments: { q: "x" } },
		]);
		expect(asst.api).toBe("anthropic-messages");
		expect(asst.provider).toBe("anthropic");
		expect(asst.model).toBe("claude-opus-4-7");

		const tr1 = msgs[2] as ToolResultMessage;
		expect(tr1.role).toBe("toolResult");
		expect(tr1.toolCallId).toBe("toolu_abc");
		expect(tr1.isError).toBe(false);
		expect(tr1.content).toEqual([{ type: "text", text: "result text" }]);

		const tr2 = msgs[3] as ToolResultMessage;
		expect(tr2.role).toBe("toolResult");
		expect(tr2.toolCallId).toBe("toolu_def");
		expect(tr2.isError).toBe(true);
		expect(tr2.content).toEqual([{ type: "text", text: "string body" }]);

		expect(msgs[4]).toMatchObject({ role: "user", content: "and another result coming" });
	});

	it("maps tool_choice variants and suppresses user wrappers that hold only tool_result", () => {
		const auto = parseRequest({
			model: "m",
			max_tokens: 8,
			tool_choice: { type: "auto" },
			messages: [{ role: "user", content: "hi" }],
		});
		expect(auto.options.toolChoice).toBe("auto");

		const named = parseRequest({
			model: "m",
			max_tokens: 8,
			tool_choice: { type: "tool", name: "lookup" },
			messages: [{ role: "user", content: "hi" }],
		});
		expect(named.options.toolChoice).toEqual({ name: "lookup" });

		const onlyResult = parseRequest({
			model: "m",
			max_tokens: 8,
			messages: [
				{
					role: "user",
					content: [{ type: "tool_result", tool_use_id: "t1", content: [{ type: "text", text: "ok" }] }],
				},
			],
		});
		// no user wrapper, just the toolResult
		expect(onlyResult.context.messages).toHaveLength(1);
		expect(onlyResult.context.messages[0]!.role).toBe("toolResult");
	});

	it("splits user text/image blocks into a separate UserMessage before a tool_result", () => {
		const parsed = parseRequest({
			model: "m",
			max_tokens: 8,
			messages: [
				{
					role: "user",
					content: [
						{ type: "text", text: "preface text" },
						{ type: "tool_result", tool_use_id: "t1", content: "ok" },
					],
				},
			],
		});
		// Expect a flush before the tool result: user("preface text") then toolResult(t1).
		expect(parsed.context.messages).toHaveLength(2);
		expect(parsed.context.messages[0]).toMatchObject({ role: "user", content: "preface text" });
		expect(parsed.context.messages[1]!.role).toBe("toolResult");
	});

	it("rejects missing required fields and unsupported request controls", () => {
		expect(() => parseRequest({})).toThrow(/model/);
		expect(() => parseRequest({ model: "m", messages: [] })).toThrow(/max_tokens/);
		expect(() => parseRequest({ model: "m", max_tokens: 1 })).toThrow(/messages/);
		const topK = parseRequest({ model: "m", max_tokens: 1, messages: [{ role: "user", content: "hi" }], top_k: 50 });
		expect(topK.options.topK).toBe(50);
		// `metadata` is tolerated permissively and surfaced on options for
		// downstream forwarding (Anthropic clients ship `metadata.user_id`).
		const withMetadata = parseRequest({
			model: "m",
			max_tokens: 1,
			messages: [{ role: "user", content: "hi" }],
			metadata: { user_id: "u_1" },
		});
		expect(withMetadata.options.extra).toBeUndefined();
		expect(withMetadata.options.metadata).toEqual({ user_id: "u_1" });
	});
});

describe("anthropic-messages encodeResponse", () => {
	it("encodes text + thinking + tool_use with correct ordering and stop_reason mapping", () => {
		const message: AssistantMessage = {
			role: "assistant",
			content: [
				{ type: "thinking", thinking: "let me think", thinkingSignature: "sig-xyz" },
				{ type: "text", text: "calling tool now" },
				{ type: "toolCall", id: "toolu_999", name: "lookup", arguments: { q: "hello" } },
			],
			api: "anthropic-messages",
			provider: "anthropic",
			model: "claude-opus-4-7",
			usage: { ...emptyUsage(), input: 12, output: 34, cacheRead: 5, cacheWrite: 7, totalTokens: 58 },
			stopReason: "toolUse",
			timestamp: 1000,
		};
		const encoded = encodeResponse(message, "claude-opus-4-7");
		expect(encoded.type).toBe("message");
		expect(encoded.role).toBe("assistant");
		expect(encoded.model).toBe("claude-opus-4-7");
		expect(encoded.stop_reason).toBe("tool_use");
		expect(encoded.stop_sequence).toBeNull();
		expect(encoded.usage).toEqual({
			input_tokens: 12,
			output_tokens: 34,
			cache_read_input_tokens: 5,
			cache_creation_input_tokens: 7,
		});
		expect(encoded.content).toEqual([
			{ type: "thinking", thinking: "let me think", signature: "sig-xyz" },
			{ type: "text", text: "calling tool now" },
			{ type: "tool_use", id: "toolu_999", name: "lookup", input: { q: "hello" } },
		]);
		expect(typeof encoded.id).toBe("string");
		expect((encoded.id as string).startsWith("msg_")).toBe(true);
	});

	it("maps stop reasons and rejects upstream terminal errors", () => {
		const base: AssistantMessage = {
			role: "assistant",
			content: [],
			api: "anthropic-messages",
			provider: "anthropic",
			model: "m",
			usage: emptyUsage(),
			stopReason: "stop",
			timestamp: 0,
		};
		expect(encodeResponse({ ...base, stopReason: "stop" }, "m").stop_reason).toBe("end_turn");
		expect(encodeResponse({ ...base, stopReason: "length" }, "m").stop_reason).toBe("max_tokens");
		expect(encodeResponse({ ...base, stopReason: "toolUse" }, "m").stop_reason).toBe("tool_use");
		expect(() => encodeResponse({ ...base, stopReason: "error", errorMessage: "upstream failed" }, "m")).toThrow(
			/upstream failed/,
		);
		expect(() => encodeResponse({ ...base, stopReason: "aborted", errorMessage: "request aborted" }, "m")).toThrow(
			/request aborted/,
		);
	});
});

describe("anthropic-messages encodeStream", () => {
	it("emits thinking_delta + signature_delta + text_delta + tool_use input_json_delta + message_stop", async () => {
		const finalMessage: AssistantMessage = {
			role: "assistant",
			content: [
				{ type: "thinking", thinking: "thoughts", thinkingSignature: "SIG" },
				{ type: "text", text: "hi there" },
				{ type: "toolCall", id: "toolu_1", name: "go", arguments: { x: 1 } },
			],
			api: "anthropic-messages",
			provider: "anthropic",
			model: "claude-opus-4-7",
			usage: { ...emptyUsage(), input: 11, output: 42, cacheRead: 3, cacheWrite: 5 },
			stopReason: "toolUse",
			timestamp: 0,
		};

		const partialAfterThinkingEnd: AssistantMessage = {
			...finalMessage,
			content: [{ type: "thinking", thinking: "thoughts", thinkingSignature: "SIG" }],
		};
		const partialAtToolStart: AssistantMessage = {
			...finalMessage,
			content: [
				{ type: "thinking", thinking: "thoughts", thinkingSignature: "SIG" },
				{ type: "text", text: "hi there" },
				{ type: "toolCall", id: "toolu_1", name: "go", arguments: {} },
			],
		};

		const events: AssistantMessageEvent[] = [
			{ type: "start", partial: finalMessage },
			{ type: "thinking_start", contentIndex: 0, partial: finalMessage },
			{ type: "thinking_delta", contentIndex: 0, delta: "thoughts", partial: finalMessage },
			{ type: "thinking_end", contentIndex: 0, content: "thoughts", partial: partialAfterThinkingEnd },
			{ type: "text_start", contentIndex: 1, partial: finalMessage },
			{ type: "text_delta", contentIndex: 1, delta: "hi ", partial: finalMessage },
			{ type: "text_delta", contentIndex: 1, delta: "there", partial: finalMessage },
			{ type: "text_end", contentIndex: 1, content: "hi there", partial: finalMessage },
			{ type: "toolcall_start", contentIndex: 2, partial: partialAtToolStart },
			{ type: "toolcall_delta", contentIndex: 2, delta: '{"x":', partial: partialAtToolStart },
			{ type: "toolcall_delta", contentIndex: 2, delta: "1}", partial: partialAtToolStart },
			{
				type: "toolcall_end",
				contentIndex: 2,
				toolCall: { type: "toolCall", id: "toolu_1", name: "go", arguments: { x: 1 } },
				partial: finalMessage,
			},
			{ type: "done", reason: "toolUse", message: finalMessage },
		];

		const sse = await collectSse(encodeStream(makeStream(events), "claude-opus-4-7"));

		// Sequence check
		const types = sse.map(e => e.event);
		expect(types).toEqual([
			"message_start",
			"content_block_start",
			"content_block_delta",
			"content_block_delta", // signature_delta
			"content_block_stop",
			"content_block_start",
			"content_block_delta",
			"content_block_delta",
			"content_block_stop",
			"content_block_start",
			"content_block_delta",
			"content_block_delta",
			"content_block_stop",
			"message_delta",
			"message_stop",
		]);

		// message_start payload
		const start = sse[0]!.data as {
			type: string;
			message: { id: string; model: string; role: string; usage: Record<string, unknown> };
		};
		expect(start.type).toBe("message_start");
		expect(start.message.model).toBe("claude-opus-4-7");
		expect(start.message.role).toBe("assistant");
		expect(start.message.id.startsWith("msg_")).toBe(true);
		expect(start.message.usage).toEqual({
			input_tokens: 11,
			output_tokens: 42,
			cache_read_input_tokens: 3,
			cache_creation_input_tokens: 5,
		});

		// thinking block_start
		expect(sse[1]!.data).toEqual({
			type: "content_block_start",
			index: 0,
			content_block: { type: "thinking", thinking: "" },
		});
		expect(sse[2]!.data).toEqual({
			type: "content_block_delta",
			index: 0,
			delta: { type: "thinking_delta", thinking: "thoughts" },
		});
		expect(sse[3]!.data).toEqual({
			type: "content_block_delta",
			index: 0,
			delta: { type: "signature_delta", signature: "SIG" },
		});
		expect(sse[4]!.data).toEqual({ type: "content_block_stop", index: 0 });

		// text block
		expect(sse[5]!.data).toEqual({
			type: "content_block_start",
			index: 1,
			content_block: { type: "text", text: "" },
		});
		expect(sse[6]!.data).toEqual({
			type: "content_block_delta",
			index: 1,
			delta: { type: "text_delta", text: "hi " },
		});

		// tool_use block
		expect(sse[9]!.data).toEqual({
			type: "content_block_start",
			index: 2,
			content_block: { type: "tool_use", id: "toolu_1", name: "go", input: {} },
		});
		expect(sse[10]!.data).toEqual({
			type: "content_block_delta",
			index: 2,
			delta: { type: "input_json_delta", partial_json: '{"x":' },
		});

		// message_delta with mapped stop_reason
		expect(sse[13]!.data).toEqual({
			type: "message_delta",
			delta: { stop_reason: "tool_use", stop_sequence: null },
			usage: {
				input_tokens: 11,
				output_tokens: 42,
				cache_read_input_tokens: 3,
				cache_creation_input_tokens: 5,
			},
		});

		expect(sse[14]!.data).toEqual({ type: "message_stop" });
	});

	it("emits an error event when the upstream stream errors", async () => {
		const errMessage: AssistantMessage = {
			role: "assistant",
			content: [],
			api: "anthropic-messages",
			provider: "anthropic",
			model: "m",
			usage: emptyUsage(),
			stopReason: "error",
			errorMessage: "boom",
			timestamp: 0,
		};
		const events: AssistantMessageEvent[] = [
			{ type: "start", partial: errMessage },
			{ type: "error", reason: "error", error: errMessage },
		];
		const sse = await collectSse(encodeStream(makeStream(events), "m"));
		const last = sse.at(-1)!;
		expect(last.event).toBe("error");
		expect(last.data).toEqual({ type: "error", error: { type: "api_error", message: "boom" } });
	});
});