import { Type } from "typebox";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getModel } from "../src/models.js";
import { streamOpenAICompletions } from "../src/providers/openai-completions.js";
import type { CacheRetention, Context, Model, Usage } from "../src/types.js";

interface CacheControl {
	type: "ephemeral";
	ttl?: string;
}

interface TextPart {
	type: "text";
	text: string;
	cache_control?: CacheControl;
}

interface CapturedParams {
	messages: Array<{ role: string; content: string | TextPart[] | null }>;
	tools?: Array<{ type: string; cache_control?: CacheControl }>;
}

const mockState = vi.hoisted(() => ({ lastParams: undefined as CapturedParams | undefined }));

const emptyUsage: Usage = {
	input: 0,
	output: 0,
	cacheRead: 0,
	cacheWrite: 0,
	totalTokens: 0,
	cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};

vi.mock("openai", () => {
	class FakeOpenAI {
		chat = {
			completions: {
				create: (params: CapturedParams) => {
					mockState.lastParams = params;
					const stream = {
						async *[Symbol.asyncIterator]() {
							yield {
								id: "chatcmpl-test",
								choices: [{ delta: {}, finish_reason: "stop" }],
								usage: {
									prompt_tokens: 100,
									completion_tokens: 10,
									prompt_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 },
									completion_tokens_details: { reasoning_tokens: 0 },
								},
							};
						},
					};
					const promise = Promise.resolve(stream) as Promise<typeof stream> & {
						withResponse: () => Promise<{ data: typeof stream; response: { status: number; headers: Headers } }>;
					};
					promise.withResponse = async () => ({
						data: stream,
						response: { status: 200, headers: new Headers() },
					});
					return promise;
				},
			},
		};
	}

	return { default: FakeOpenAI };
});

function customModel(compat: Model<"openai-completions">["compat"]): Model<"openai-completions"> {
	return {
		id: "custom-anthropic-proxy",
		name: "Custom Anthropic Proxy",
		api: "openai-completions",
		provider: "openrouter",
		baseUrl: "https://example.com/v1",
		reasoning: true,
		input: ["text"],
		cost: { input: 4, output: 12, cacheRead: 0.4, cacheWrite: 9 },
		contextWindow: 128000,
		maxTokens: 32000,
		compat,
	};
}

async function capturePayload(
	model: Model<"openai-completions">,
	options?: { cacheRetention?: CacheRetention },
	messages?: Context["messages"],
): Promise<CapturedParams> {
	await streamOpenAICompletions(
		model,
		{
			systemPrompt: "System prompt",
			messages: messages ?? [{ role: "user", content: "Hello", timestamp: Date.now() }],
			tools: [{ name: "read", description: "Read a file", parameters: Type.Object({ path: Type.String() }) }],
		},
		{ apiKey: "test-key", ...options },
	).result();

	if (!mockState.lastParams) throw new Error("Expected payload to be captured");
	return mockState.lastParams;
}

function instructionMessage(params: CapturedParams) {
	return params.messages.find((message) => message.role === "system" || message.role === "developer");
}

describe("openai-completions cacheControlFormat", () => {
	beforeEach(() => {
		mockState.lastParams = undefined;
	});

	// Marker placement is the contract: system/instruction block, tool definitions and the final
	// message each carry cache_control, with the ttl only when long retention is actually supported.
	it.each([
		{ name: "model compat opts in", model: () => customModel({ cacheControlFormat: "anthropic" }), ttl: undefined },
		{
			name: "OpenRouter Anthropic route",
			model: () => getModel("openrouter", "anthropic/claude-sonnet-4"),
			ttl: undefined,
		},
		{
			name: "Prime Inference Anthropic route",
			model: () => getModel("prime-inference", "anthropic/claude-fable-5"),
			ttl: undefined,
		},
		{
			name: "Prime Inference Anthropic route with long retention",
			model: () => getModel("prime-inference", "anthropic/claude-fable-5"),
			retention: "long" as const,
			ttl: "1h",
		},
		{
			name: "OpenRouter Anthropic route with long retention",
			model: () => getModel("openrouter", "anthropic/claude-sonnet-4"),
			retention: "long" as const,
			ttl: "1h",
		},
		{
			name: "long retention on a model that cannot apply it",
			model: () => customModel({ cacheControlFormat: "anthropic", supportsLongCacheRetention: false }),
			retention: "long" as const,
			ttl: undefined,
		},
	])("marks the instruction block, tools and last message: $name", async ({ model, retention, ttl }) => {
		const expected: CacheControl = ttl ? { type: "ephemeral", ttl } : { type: "ephemeral" };
		const params = await capturePayload(model(), { cacheRetention: retention });

		const instruction = instructionMessage(params);
		expect(Array.isArray(instruction?.content)).toBe(true);
		expect((instruction?.content as TextPart[])[0]?.cache_control).toEqual(expected);
		expect(params.tools).toHaveLength(1);
		expect(params.tools?.[0]?.cache_control).toEqual(expected);
		const lastMessage = params.messages[params.messages.length - 1];
		expect(lastMessage.role).toBe("user");
		expect((lastMessage.content as TextPart[])[0]?.cache_control).toEqual(expected);
	});

	it.each([
		{
			name: "a non-Anthropic Prime Inference model",
			model: () => getModel("prime-inference", "openai/gpt-5.6-sol"),
			retention: undefined,
		},
		{
			name: "cacheRetention none",
			model: () => customModel({ cacheControlFormat: "anthropic" }),
			retention: "none" as const,
		},
	])("omits cache markers for $name", async ({ model, retention }) => {
		const params = await capturePayload(model(), { cacheRetention: retention });

		expect(Array.isArray(instructionMessage(params)?.content)).toBe(false);
		expect(params.tools?.[0]?.cache_control).toBeUndefined();
		expect(typeof params.messages[params.messages.length - 1]?.content).toBe("string");
	});

	it("advances the cache marker to the trailing tool result", async () => {
		const model = getModel("prime-inference", "anthropic/claude-haiku-4.5");
		const now = Date.now();
		const params = await capturePayload(model, undefined, [
			{ role: "user", content: "Read the file", timestamp: now },
			{
				role: "assistant",
				content: [{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "file.txt" } }],
				api: model.api,
				provider: model.provider,
				model: model.id,
				usage: emptyUsage,
				stopReason: "toolUse",
				timestamp: now + 1,
			},
			{
				role: "toolResult",
				toolCallId: "tool-1",
				toolName: "read",
				content: [{ type: "text", text: "file contents" }],
				isError: false,
				timestamp: now + 2,
			},
		]);

		expect(params.messages.at(-1)).toMatchObject({
			role: "tool",
			content: [{ type: "text", text: "file contents", cache_control: { type: "ephemeral" } }],
		});
	});
});
