import { afterEach, describe, expect, it, vi } from "vitest";
import { getModel } from "../src/models.js";
import { streamOpenAIResponses } from "../src/providers/openai-responses.js";
import type { Model } from "../src/types.js";

type CapturedHeaders = Headers | string[][] | Record<string, string | readonly string[]> | undefined;

function getHeader(headers: CapturedHeaders, name: string): string | null {
	if (!headers) return null;
	if (headers instanceof Headers) return headers.get(name);
	const lowerName = name.toLowerCase();
	if (Array.isArray(headers)) {
		return headers.find(([key]) => key?.toLowerCase() === lowerName)?.[1] ?? null;
	}
	for (const [key, value] of Object.entries(headers)) {
		if (key.toLowerCase() === lowerName) return typeof value === "string" ? value : value.join(", ");
	}
	return null;
}

const proxyModel = (compat?: Model<"openai-responses">["compat"]): Model<"openai-responses"> => ({
	...getModel("openai", "gpt-5.4"),
	provider: "opencode",
	baseUrl: "https://proxy.example.com/v1",
	...(compat ? { compat } : {}),
});

/** Drives one request against a stubbed SSE endpoint and returns the payload plus request headers. */
async function captureRequest(
	model: Model<"openai-responses">,
	options: Parameters<typeof streamOpenAIResponses>[2] = {},
): Promise<{ payload: unknown; sessionId: string | null; clientRequestId: string | null }> {
	const captured = {
		payload: undefined as unknown,
		sessionId: null as string | null,
		clientRequestId: null as string | null,
	};
	vi.spyOn(globalThis, "fetch").mockImplementation(async (_input, init) => {
		captured.sessionId = getHeader(init?.headers, "session_id");
		captured.clientRequestId = getHeader(init?.headers, "x-client-request-id");
		return new Response("data: [DONE]\n\n", { status: 200, headers: { "content-type": "text/event-stream" } });
	});

	const stream = streamOpenAIResponses(
		model,
		{ systemPrompt: "sys", messages: [{ role: "user", content: "hi", timestamp: Date.now() }] },
		{
			apiKey: "test-key",
			...options,
			onPayload: (payload) => {
				captured.payload = payload;
			},
		},
	);
	for await (const event of stream) {
		if (event.type === "done" || event.type === "error") break;
	}
	return captured;
}

// "none" = the model accepts an explicit off switch; "absent" = the field must not be serialized.
const REASONING_DEFAULTS: Array<{
	provider: "openai" | "github-copilot";
	modelId: string;
	effort: "none" | "absent";
	model: () => Model<"openai-responses">;
}> = [
	{
		provider: "github-copilot",
		modelId: "gpt-5-mini",
		effort: "absent",
		model: () => getModel("github-copilot", "gpt-5-mini"),
	},
	...(["gpt-5.1", "gpt-5.2", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.5"] as const).map(
		(modelId) => ({
			provider: "openai" as const,
			modelId,
			effort: "none" as const,
			model: () => getModel("openai", modelId),
		}),
	),
	...(["gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5-pro", "gpt-5.2-pro", "gpt-5.4-pro", "gpt-5.5-pro"] as const).map(
		(modelId) => ({
			provider: "openai" as const,
			modelId,
			effort: "absent" as const,
			model: () => getModel("openai", modelId),
		}),
	),
];

describe("openai-responses provider defaults", () => {
	afterEach(() => {
		vi.restoreAllMocks();
	});

	it.each(REASONING_DEFAULTS)(
		"serializes $effort reasoning effort for $provider $modelId when no reasoning is requested",
		async ({ effort, model }) => {
			const { payload } = await captureRequest(model());

			if (effort === "none") {
				expect(payload).toMatchObject({ reasoning: { effort: "none" } });
			} else {
				expect(payload).not.toMatchObject({ reasoning: expect.anything() });
			}
		},
	);

	it.each([
		{
			name: "official OpenAI Responses requests with a sessionId",
			model: () => getModel("openai", "gpt-5.4"),
			options: { sessionId: "session-123" },
			expected: { sessionId: "session-123", clientRequestId: "session-123" },
		},
		{
			name: "proxy Responses requests with a sessionId",
			model: () => proxyModel(),
			options: { sessionId: "session-123" },
			expected: { sessionId: "session-123", clientRequestId: "session-123" },
		},
		{
			name: "a model that opts out of the session_id header",
			model: () => proxyModel({ sendSessionIdHeader: false }),
			options: { sessionId: "session-123" },
			expected: { sessionId: null, clientRequestId: "session-123" },
		},
		{
			name: "explicit header overrides",
			model: () => getModel("openai", "gpt-5.4"),
			options: {
				sessionId: "session-123",
				headers: { session_id: "override-session", "x-client-request-id": "override-request" },
			},
			expected: { sessionId: "override-session", clientRequestId: "override-request" },
		},
		{
			name: "cacheRetention none",
			model: () => getModel("openai", "gpt-5.4"),
			options: { cacheRetention: "none" as const, sessionId: "session-123" },
			expected: { sessionId: null, clientRequestId: null },
		},
	])("sends cache-affinity headers for $name", async ({ model, options, expected }) => {
		const { sessionId, clientRequestId } = await captureRequest(model(), options);

		expect({ sessionId, clientRequestId }).toEqual(expected);
	});

	it.each([
		["github-copilot" as const, "auto" as const, false],
		["github-copilot" as const, "default" as const, false],
		["openai" as const, "default" as const, true],
	])("scopes service_tier serialization to the provider (%s, %s)", async (provider, serviceTier, expected) => {
		const model = { ...getModel("openai", "gpt-5.4"), provider };
		const sse = `data: ${JSON.stringify({
			type: "response.completed",
			response: {
				status: "completed",
				usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2, input_tokens_details: { cached_tokens: 0 } },
			},
		})}\n\n`;
		let wireBody: Record<string, unknown> | undefined;
		vi.spyOn(globalThis, "fetch").mockImplementation(async (_input, init) => {
			wireBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
			return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
		});

		const result = await streamOpenAIResponses(
			model,
			{ systemPrompt: "sys", messages: [{ role: "user", content: "hi", timestamp: Date.now() }] },
			{ apiKey: "test-key", serviceTier },
		).result();

		expect(result.stopReason).toBe("stop");
		// Copilot rejects the FIELD for every value; elsewhere absence means "auto"
		// (the project tier), so an explicit "default" must stay on the wire.
		expect(wireBody && "service_tier" in wireBody).toBe(expected);
		if (expected) {
			expect((wireBody as Record<string, unknown>).service_tier).toBe(serviceTier);
		}
	});
});
