import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { normalizeContext, type ModelsStoreEntry, type Provider } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { selectChatApi } from "../src/protocol.ts";
import { parseCatalogPage, toExplabsModel } from "../src/catalog.ts";
import experientialLabs, { rebaseCachedExplabsModels } from "../src/index.ts";
import type { GatewayMessage } from "../src/gateway.ts";

describe("automatic chat protocol selection", () => {
	for (const [model, api] of [
		[{ id: "claude-fable-5.1" }, "anthropic-messages"],
		[{ id: "claude-haiku-5.5", protocolHints: { defaultProvider: "openrouter" } }, "anthropic-messages"],
		[{ id: "custom-claude", protocolHints: { maker: "Anthropic", defaultProvider: "bedrock" } }, "anthropic-messages"],
		[{ id: "custom", protocolHints: { defaultProvider: "anthropic" } }, "anthropic-messages"],
		[{ id: "gpt-6-luna" }, "openai-responses"],
		[{ id: "o3" }, "openai-responses"],
		[{ id: "alias", protocolHints: { maker: "OpenAI", defaultProvider: "experiential_cloud" } }, "openai-responses"],
		[{ id: "custom", nativeOpenAIRoute: true }, "openai-responses"],
		[{ id: "custom", protocolHints: { defaultProvider: "openai" } }, "openai-responses"],
		[{ id: "gemini-3.7-flash", protocolHints: { maker: "Google" } }, "openai-completions"],
		[{ id: "deepseek-flash" }, "openai-completions"],
		[{ id: "qwen3.8-27b" }, "openai-completions"],
		[{ id: "gpt-oss-120b", protocolHints: { maker: "OpenAI", defaultProvider: "groq" } }, "openai-completions"],
		[{ id: "mistral-nemo" }, "openai-completions"],
		[{ id: "custom", protocolHints: { defaultProvider: "openrouter" } }, "openai-completions"],
		[{ id: "unknown-custom-model" }, "openai-completions"],
	] as const) {
		it(`selects ${api} for ${JSON.stringify(model)}`, () => assert.equal(selectChatApi(model), api));
	}

	it("persists maker and the ordered first route rather than treating list order as the waterfall", () => {
		const row = parseCatalogPage({ models: [{ model: { slug: "custom", maker: "Anthropic", input_modalities: ["text"], output_modalities: ["text"], supported_params: { tools: true } },
			providers: [{ id: "other", provider: "openai" }, { id: "first", provider: "bedrock" }], default_provider_ids: ["first", "other"] }] }).models[0];
		const model = toExplabsModel(row.model, row.providers, "https://gateway.example/v1", row.default_provider_ids);
		assert.ok(model && model.type !== "classifier");
		assert.equal(model.api, "anthropic-messages");
		assert.deepEqual(model.protocolHints, { maker: "Anthropic", defaultProvider: "bedrock" });
		assert.equal(model.nativeOpenAIRoute, false);
		const cached = rebaseCachedExplabsModels(JSON.parse(JSON.stringify({ models: [model], checkedAt: 1 })), "https://new.example/api/v1");
		assert.equal(cached?.models[0].api, model.api);
		assert.equal(cached?.models[0].baseUrl, "https://new.example/api");
	});

	it("migrates mixed legacy caches individually and removes incompatible protocol flags", () => {
		const original = { checkedAt: 1, models: [
			{ provider: "explabs", id: "claude-haiku-5.5", api: "openai-responses", baseUrl: "https://old.example/v1", compat: { supportsDeveloperRole: true, supportsLongCacheRetention: true } },
			{ provider: "explabs", id: "gpt-6-luna", api: "anthropic-messages", baseUrl: "https://old.example", compat: { forceAdaptiveThinking: true } },
			{ provider: "explabs", id: "deepseek-flash", api: "anthropic-messages", baseUrl: "https://old.example", compat: { forceAdaptiveThinking: true } },
			{ provider: "explabs", id: "jev-latest", type: "classifier", api: "typesafe-system-one", baseUrl: "https://old.example/v1" },
			{ provider: "explabs", id: "gpt-6-luna-decisions", type: "classifier", api: "typesafe-system-one", baseUrl: "https://old.example/v1", protocolHints: { defaultProvider: "openai" } },
			{ provider: "other", id: "claude-haiku-5.5", api: "openai-responses", baseUrl: "https://other.example/v1" },
		] } as unknown as ModelsStoreEntry;
		const cached = rebaseCachedExplabsModels(original, "https://new.example/v1", new Set(["claude-haiku-5-5"]));
		assert.deepEqual(cached?.models.map(m => [m.api, m.baseUrl]), [
			["anthropic-messages", "https://new.example"], ["openai-responses", "https://new.example/v1"],
			["openai-completions", "https://new.example/v1"], ["typesafe-system-one", "https://new.example/v1"],
			["openai-decisions", "https://new.example/v1"], ["openai-responses", "https://other.example/v1"],
		]);
		const migrated = cached?.models as unknown as { compat?: Record<string, unknown> }[];
		assert.equal(migrated[0].compat?.supportsLongCacheRetention, undefined);
		assert.equal(migrated[0].compat?.forceAdaptiveThinking, true);
		assert.equal(migrated[0].compat?.supportsDeveloperRole, undefined);
		assert.equal(migrated[1].compat?.forceAdaptiveThinking, undefined);
		assert.equal(migrated[2].compat?.forceAdaptiveThinking, undefined);
		assert.equal(migrated[2].compat?.thinkingFormat, "openai");
		assert.equal(original.models[0].api, "openai-responses");
		assert.equal(original.models[0].baseUrl, "https://old.example/v1");
	});
});

it("one provider dispatches each discovered chat model to its selected wire protocol", async t => {
	let registered: Provider | undefined;
	experientialLabs({ registerProvider: (provider: Provider) => { registered = provider; }, registerCommand: () => {}, on: () => {} } as unknown as ExtensionAPI);
	assert.ok(registered?.refreshModels);
	const provider = registered;
	const definitions = [
		{ slug: "claude-haiku-5.5", maker: "Anthropic", upstream: "anthropic", reasoning: true },
		{ slug: "gpt-6-astra", maker: "OpenAI", upstream: "openai", reasoning: true },
		{ slug: "mistral-nemo", maker: "Mistral", upstream: "experiential_cloud", reasoning: false },
	];
	t.mock.method(globalThis, "fetch", async () => Response.json({ total: 3, models: definitions.map(m => ({
		model: { slug: m.slug, maker: m.maker, status: "active", input_modalities: ["text"], output_modalities: ["text"], supported_params: { tools: true, reasoning: m.reasoning } },
		providers: [{ id: "first", provider: m.upstream, capabilities: { supports_developer_messages: true, supported_reasoning_efforts: ["none", "low"] } }], default_provider_ids: ["first"],
	})) }));
	await provider.refreshModels!({ credential: { type: "api_key", key: "xpl_test" }, allowNetwork: true, signal: new AbortController().signal,
		publish: async ({ update }) => { update?.(); return true; } });
	const models = provider.getModels();
	assert.deepEqual(models.map(m => m.api), ["anthropic-messages", "openai-responses", "openai-completions"]);
	for (const model of models) {
		const stream = provider.streamSimple!(model, normalizeContext({ messages: [{ role: "user", content: "Reply OK", timestamp: 0 }] }), {
			apiKey: "xpl_test", maxTokens: 32, maxRetries: 0, reasoning: "low",
			fetch: async (input, init) => {
				const url = new URL(input instanceof Request ? input.url : String(input));
				const body = init?.body ?? (input instanceof Request ? await input.clone().text() : "");
				const payload = JSON.parse(String(body));
				assert.equal(payload.model, model.id);
				let events: Record<string, unknown>[];
				if (model.api === "anthropic-messages") {
					assert.equal(url.pathname, "/v1/messages");
					assert.equal(payload.thinking.type, "adaptive");
					assert.equal(payload.output_config.effort, "low");
					events = [
						{ type: "message_start", message: { id: "msg_1", type: "message", role: "assistant", model: model.id, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 0 } } },
						{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
						{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "OK" } },
						{ type: "content_block_stop", index: 0 },
						{ type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { input_tokens: 10, output_tokens: 2, cost: 0.001 } }, { type: "message_stop" },
					];
				} else if (model.api === "openai-responses") {
					assert.equal(url.pathname, "/v1/responses");
					assert.equal(payload.reasoning.effort, "low");
					assert.ok(payload.include.includes("reasoning.encrypted_content"));
					const item = { type: "message", id: "msg_1", role: "assistant", status: "completed", content: [{ type: "output_text", text: "OK", annotations: [] }] };
					events = [
						{ type: "response.created", response: { id: "resp_1", status: "in_progress" } },
						{ type: "response.output_item.added", output_index: 0, item: { ...item, content: [] } },
						{ type: "response.output_text.delta", output_index: 0, content_index: 0, delta: "OK" },
						{ type: "response.output_item.done", output_index: 0, item },
						{ type: "response.completed", response: { id: "resp_1", status: "completed", output: [item], usage: { input_tokens: 10, output_tokens: 2, cost: 0.001 } } },
					];
				} else {
					assert.equal(url.pathname, "/v1/chat/completions");
					assert.equal(payload.messages[0].role, "user");
					assert.equal(payload.max_tokens, 32);
					events = [
						{ id: "chat_1", object: "chat.completion.chunk", created: 0, model: model.id, choices: [{ index: 0, delta: { role: "assistant", content: "OK" }, finish_reason: null }] },
						{ id: "chat_1", object: "chat.completion.chunk", created: 0, model: model.id, choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 10, completion_tokens: 2, cost: 0.001 } },
					];
				}
				return new Response(events.map(e => `${typeof e.type === "string" && model.api === "anthropic-messages" ? `event: ${e.type}\n` : ""}data: ${JSON.stringify(e)}\n\n`).join(""), {
					headers: { "content-type": "text/event-stream", "x-request-id": `req-${model.id}`, "x-gateway-provider": "actual-rung" },
				});
			},
		});
		const result = await stream.result() as GatewayMessage;
		assert.equal(result.stopReason, "stop", result.errorMessage);
		assert.equal(result.api, model.api);
		assert.equal(result.content.length, 1);
		assert.equal(result.content[0].type, "text");
		assert.ok(result.content[0].type === "text" && result.content[0].text === "OK");
		assert.equal(result.explabs?.chargedUsd, 0.001);
		assert.equal(result.explabs?.requestId, `req-${model.id}`);
	}
});
