import { describe, expect, it } from "vitest";
import { transformMessages } from "../src/providers/transform-messages.js";
import type { AssistantMessage, Message, Model, StopReason, ToolCall, ToolResultMessage } from "../src/types.js";

const model: Model<"anthropic-messages"> = {
	id: "fixture",
	name: "Fixture",
	api: "anthropic-messages",
	provider: "anthropic",
	baseUrl: "http://localhost",
	reasoning: false,
	input: ["text", "image"],
	cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
	contextWindow: 8192,
	maxTokens: 1024,
};

function assistant(stopReason: StopReason, ...ids: string[]): AssistantMessage {
	return {
		role: "assistant",
		content: ids.map((id) => ({ type: "toolCall", id, name: "fixture_tool", arguments: {} })),
		api: model.api,
		provider: model.provider,
		model: model.id,
		usage: {
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0,
			totalTokens: 0,
			cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
		},
		stopReason,
		timestamp: 0,
	};
}

function toolResult(toolCallId: string, text = "completed"): ToolResultMessage {
	return {
		role: "toolResult",
		toolCallId,
		toolName: "fixture_tool",
		content: [{ type: "text", text }],
		isError: false,
		timestamp: 0,
	};
}

const user: Message = { role: "user", content: "Run fixture", timestamp: 0 };

describe.each([false, true])("interrupted tool history (cross-provider: %s)", (crossProvider) => {
	function transform(messages: Message[]): Message[] {
		const target = crossProvider ? { ...model, provider: "other-provider" } : model;
		return transformMessages(messages, target, (id) => id.replace(/[^a-zA-Z0-9_-]/g, "_"));
	}

	it.each(["aborted", "error"] as const)("omits results from an %s assistant message", (reason) => {
		const messages = [user, assistant(reason, "call|1"), toolResult("call|1")];
		const original = structuredClone(messages);

		expect(transform(messages)).toEqual([user]);
		expect(messages).toEqual(original);
	});

	it("preserves a completed tool exchange", () => {
		const messages = [user, assistant("toolUse", "call|1"), toolResult("call|1")];
		const expectedId = crossProvider ? "call_1" : "call|1";

		expect(transform(messages)).toEqual([user, assistant("toolUse", expectedId), toolResult(expectedId)]);
	});

	it("omits every result from consecutive interrupted turns", () => {
		const messages = [
			user,
			assistant("aborted", "call|1", "call|2"),
			toolResult("call|2"),
			assistant("error", "call|3"),
			toolResult("call|3"),
			toolResult("call|1"),
		];

		expect(transform(messages)).toEqual([user]);
	});

	it.each(["aborted", "error"] as const)("keeps valid exchanges before and after an %s turn", (reason) => {
		const before = [assistant("toolUse", "before|1"), toolResult("before|1")];
		const retry = [assistant("toolUse", "call|1"), toolResult("call|1", "retry completed")];
		const messages = [
			user,
			...before,
			assistant(reason, "call|1", "call|2"),
			toolResult("call|1", "interrupted result"),
			user,
			...retry,
			toolResult("call|2", "late interrupted result"),
		];

		expect(transform(messages)).toEqual(transform([user, ...before, user, ...retry]));
	});
});

describe("tool-result pairing boundaries", () => {
	it("fills missing results for surviving calls around an interrupted turn", () => {
		const messages = [
			user,
			assistant("toolUse", "before"),
			assistant("aborted", "interrupted"),
			toolResult("interrupted"),
			assistant("toolUse", "after", "missing"),
			toolResult("after"),
		];

		expect(transformMessages(messages, model)).toEqual([
			user,
			assistant("toolUse", "before"),
			{ ...toolResult("before", "No result provided"), isError: true, timestamp: expect.any(Number) },
			assistant("toolUse", "after", "missing"),
			toolResult("after"),
			{ ...toolResult("missing", "No result provided"), isError: true, timestamp: expect.any(Number) },
		]);
	});

	it("omits results without a call in the preceding assistant turn", () => {
		const messages = [
			user,
			toolResult("unknown"),
			assistant("toolUse", "finished"),
			toolResult("finished"),
			user,
			toolResult("finished", "late duplicate"),
		];

		expect(transformMessages(messages, model)).toEqual([
			user,
			assistant("toolUse", "finished"),
			toolResult("finished"),
			user,
		]);
	});

	it("pulls a real late result before the user messages that intervene after its turn", () => {
		const interrupted: Message = { role: "user", content: "<prime_agent_update_interrupted>", timestamp: 0 };
		const resume: Message = { role: "user", content: "continue task", timestamp: 0 };
		const messages = [
			user,
			assistant("toolUse", "call|1"),
			interrupted,
			{ ...toolResult("call|1", "Request was aborted"), isError: true },
			resume,
		];

		const result = transformMessages(messages, model);
		expect(result.map((message) => message.role)).toEqual(["user", "assistant", "toolResult", "user", "user"]);
		expect(result[2]).toMatchObject({ toolCallId: "call|1", isError: true });
		expect(result[3]).toMatchObject({ content: [{ type: "text", text: "<prime_agent_update_interrupted>" }] });
		expect(result[4]).toMatchObject({ content: [{ type: "text", text: "continue task" }] });
	});
});

describe("cross-model session migration", () => {
	const copilotClaude: Model<"anthropic-messages"> = {
		...model,
		id: "claude-sonnet-4.5",
		provider: "github-copilot",
		baseUrl: "https://api.individual.githubcopilot.com",
		reasoning: true,
	};

	function fromOtherModel(api: AssistantMessage["api"], content: AssistantMessage["content"]): AssistantMessage {
		return { ...assistant("toolUse"), api, provider: "github-copilot", model: "gpt-5", content };
	}

	function migrate(messages: Message[]): Message[] {
		return transformMessages(messages, copilotClaude, (id) => id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64));
	}

	it("converts thinking blocks to plain text when the source model differs", () => {
		const result = migrate([
			user,
			fromOtherModel("openai-completions", [
				{ type: "thinking", thinking: "Let me think about this...", thinkingSignature: "reasoning_content" },
				{ type: "text", text: "Hi there!" },
			]),
		]);

		const migrated = result.find((message) => message.role === "assistant") as AssistantMessage;
		expect(migrated.content.filter((block) => block.type === "thinking")).toHaveLength(0);
		expect(migrated.content.filter((block) => block.type === "text").length).toBeGreaterThanOrEqual(2);
	});

	it("removes thoughtSignature from tool calls when migrating between models", () => {
		const result = migrate([
			user,
			fromOtherModel("openai-responses", [
				{
					type: "toolCall",
					id: "call_123",
					name: "bash",
					arguments: { command: "ls" },
					thoughtSignature: JSON.stringify({ type: "reasoning.encrypted", id: "call_123", data: "encrypted" }),
				},
			]),
			toolResult("call_123"),
		]);

		const migrated = result.find((message) => message.role === "assistant") as AssistantMessage;
		expect(
			(migrated.content.find((block) => block.type === "toolCall") as ToolCall).thoughtSignature,
		).toBeUndefined();
	});

	it("adds synthetic results for trailing orphaned tool calls across the migration", () => {
		const result = migrate([
			user,
			fromOtherModel("openai-responses", [
				{ type: "toolCall", id: "call_1|fc_1", name: "read", arguments: { path: "README.md" } },
				{ type: "toolCall", id: "call_2|fc_2", name: "bash", arguments: { command: "pwd" } },
			]),
			{ ...toolResult("call_1|fc_1"), toolName: "read" },
		]);

		const synthetic = result.filter((message) => message.role === "toolResult" && message.isError);
		expect(synthetic).toHaveLength(1);
		expect(synthetic[0]).toMatchObject({
			toolCallId: "call_2_fc_2",
			toolName: "bash",
			content: [{ type: "text", text: "No result provided" }],
		});
	});
});
