/**
 * Repro for #827 — `opencode-go/kimi-k2.6` returns 400 with
 * `tool_choice 'specified' is incompatible with thinking enabled`
 * whenever the agent forces a tool call while reasoning is on.
 *
 * The fix follows the Anthropic pattern (`disableThinkingIfToolChoiceForced`)
 * — when a forced tool_choice is sent to a Kimi reasoning model, we strip
 * reasoning for that single turn rather than dropping `tool_choice` outright.
 */
import { afterEach, describe, expect, it } from "bun:test";
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
import { streamOpenAICompletions } from "@oh-my-pi/pi-ai/providers/openai-completions";
import type { Context, Model, Tool } from "@oh-my-pi/pi-ai/types";
import * as z from "zod/v4";

const originalFetch = global.fetch;

afterEach(() => {
	global.fetch = originalFetch;
});

const echoTool: Tool = {
	name: "echo",
	description: "Echo input",
	parameters: z.object({ text: z.string() }),
};

const ctx: Context = {
	messages: [{ role: "user", content: "do it", timestamp: Date.now() }],
	tools: [echoTool],
};

function abortedSignal(): AbortSignal {
	const controller = new AbortController();
	controller.abort();
	return controller.signal;
}

function kimiOpencodeGoModel(): Model<"openai-completions"> {
	return {
		...getBundledModel("openai", "gpt-4o-mini"),
		api: "openai-completions",
		provider: "opencode-go",
		baseUrl: "https://opencode.ai/zen/v1",
		id: "kimi-k2.6",
		name: "Kimi K2.6",
		reasoning: true,
	};
}

function kimiOpenRouterModel(): Model<"openai-completions"> {
	return {
		...getBundledModel("openai", "gpt-4o-mini"),
		api: "openai-completions",
		provider: "openrouter",
		baseUrl: "https://openrouter.ai/api/v1",
		id: "moonshotai/kimi-k2",
		name: "Kimi K2 (OpenRouter)",
		reasoning: true,
	};
}

function captureBody(
	model: Model<"openai-completions">,
	opts: Parameters<typeof streamOpenAICompletions>[2],
): Promise<unknown> {
	const { promise, resolve } = Promise.withResolvers<unknown>();
	streamOpenAICompletions(model, ctx, {
		...opts,
		apiKey: "test-key",
		signal: abortedSignal(),
		onPayload: payload => resolve(payload),
	});
	return promise;
}

interface CompletionsBody {
	tool_choice?: unknown;
	tools?: unknown[];
	reasoning_effort?: unknown;
	reasoning?: unknown;
	thinking?: unknown;
}

describe("issue #827 — kimi reasoning models drop reasoning under forced tool_choice", () => {
	it("strips reasoning_effort when toolChoice is forced on direct Kimi (Moonshot-style id)", async () => {
		const body = (await captureBody(kimiOpencodeGoModel(), {
			reasoning: "high",
			toolChoice: "any",
		})) as CompletionsBody;

		// Forced choice still forwarded so the model must pick a tool…
		expect(body.tool_choice).toBe("required");
		// …but reasoning is suppressed to satisfy Kimi's "thinking incompatible with forced tool_choice" rule.
		expect(body.reasoning_effort).toBeUndefined();
	});

	it("preserves reasoning_effort when toolChoice is auto", async () => {
		const body = (await captureBody(kimiOpencodeGoModel(), {
			reasoning: "high",
			toolChoice: "auto",
		})) as CompletionsBody;

		expect(body.tool_choice).toBe("auto");
		expect(body.reasoning_effort).toBe("high");
	});

	it("strips OpenRouter-shaped reasoning object on forced toolChoice for Kimi via OpenRouter", async () => {
		const body = (await captureBody(kimiOpenRouterModel(), {
			reasoning: "high",
			toolChoice: { type: "tool", name: "echo" },
		})) as CompletionsBody;

		expect(body.tool_choice).toMatchObject({ type: "function", function: { name: "echo" } });
		expect(body.reasoning).toBeUndefined();
		expect(body.reasoning_effort).toBeUndefined();
	});
	it("sends explicit thinking disabled for Moonshot Kimi K2.6 when a named tool is forced", async () => {
		const model: Model<"openai-completions"> = {
			...getBundledModel("openai", "gpt-4o-mini"),
			api: "openai-completions",
			provider: "moonshot",
			baseUrl: "https://api.moonshot.ai/v1",
			id: "kimi-k2.6",
			name: "Kimi K2.6",
			reasoning: false,
		};
		const body = (await captureBody(model, {
			toolChoice: { type: "tool", name: "echo" },
		})) as CompletionsBody;

		expect(body.tool_choice).toMatchObject({ type: "function", function: { name: "echo" } });
		expect(body.thinking).toEqual({ type: "disabled" });
		expect(body.reasoning).toBeUndefined();
		expect(body.reasoning_effort).toBeUndefined();
	});

	it("strips reasoning_effort for Anthropic Claude models served via openai-completions (e.g. LiteLLM/OpenRouter proxies)", async () => {
		// LiteLLM / Vertex proxies often expose Claude through chat-completions; Anthropic
		// itself rejects reasoning + forced tool_choice (see anthropic.ts:disableThinkingIfToolChoiceForced),
		// so the same constraint must follow the model when it's reached through the OpenAI shape.
		const model: Model<"openai-completions"> = {
			...getBundledModel("openai", "gpt-4o-mini"),
			api: "openai-completions",
			provider: "litellm",
			baseUrl: "http://localhost:4000/v1",
			id: "claude-sonnet-4-6",
			name: "Claude Sonnet 4.6 (LiteLLM)",
			reasoning: true,
		};

		const body = (await captureBody(model, {
			reasoning: "high",
			toolChoice: "any",
		})) as CompletionsBody;

		expect(body.tool_choice).toBe("required");
		expect(body.reasoning_effort).toBeUndefined();
	});
	it("does not strip reasoning on non-Kimi models even with forced tool_choice", async () => {
		// Non-kimi reasoning model — OpenAI itself accepts forced tool_choice with reasoning.
		const model: Model<"openai-completions"> = {
			...getBundledModel("openai", "gpt-4o-mini"),
			api: "openai-completions",
			id: "gpt-5-mini",
			reasoning: true,
		};

		const body = (await captureBody(model, {
			reasoning: "high",
			toolChoice: "any",
		})) as CompletionsBody;

		expect(body.tool_choice).toBe("required");
		expect(body.reasoning_effort).toBe("high");
	});
});