import assert from "node:assert/strict";
import { describe, it, type TestContext } from "node:test";
import type { ClassifierApi, ClassifierContext, ClassifierModel, Provider } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { parseCatalogPage, toExplabsModel } from "../src/catalog.ts";
import experientialLabs, { rebaseCachedExplabsModels } from "../src/index.ts";

const lunaRow = {
	model: { slug: "gpt-6-luna-decisions", display_name: "GPT-6 Luna Decisions", status: "active",
		context_window: 922000, input_modalities: ["text", "image"], output_modalities: ["text"],
		supported_params: { reasoning: false } },
	providers: [{ provider: "openai", billing_source: "host_managed", input_nano_usd_per_million: 100000000,
		cached_input_nano_usd_per_million: 100000000, output_nano_usd_per_million: 0,
		capabilities: { supports_decisions: true, supports_tools: false } }],
};

async function registeredProvider(t: TestContext): Promise<{ provider: Provider; model: ClassifierModel<ClassifierApi> }> {
	let provider: Provider | undefined;
	experientialLabs({ registerProvider: (value: Provider) => { provider = value; }, on: () => {}, registerCommand: () => {} } as unknown as ExtensionAPI);
	assert.ok(provider?.refreshModels);
	t.mock.method(globalThis, "fetch", async () => Response.json({ models: [lunaRow], total: 1 }));
	await provider.refreshModels({ credential: { type: "api_key", key: "xpl_test" }, allowNetwork: true,
		signal: new AbortController().signal, publish: async ({ update }) => { update?.(); return true; } });
	assert.deepEqual(provider.getModels(), []);
	const model = provider.getAllModels?.()[0];
	assert.ok(model?.type === "classifier" && model.api === "openai-decisions");
	return { provider, model };
}

describe("Experiential Labs Decisions", () => {
	it("maps and migrates Luna Decisions without changing System One or chat Luna", () => {
		const row = parseCatalogPage({ models: [lunaRow] }).models[0];
		const model = toExplabsModel(row.model, row.providers, "https://gateway.example/v1", []);
		assert.equal(model?.type, "classifier");
		assert.equal(model?.api, "openai-decisions");
		assert.equal(model?.baseUrl, "https://gateway.example/v1");
		assert.deepEqual(model?.input, ["text", "image"]);
		assert.equal(model?.contextWindow, 922000);
		assert.deepEqual(model?.cost, { input: 0.1, output: 0, cacheRead: 0.1, cacheWrite: 0 });
		assert.ok(model?.type === "classifier");
		const cached = rebaseCachedExplabsModels({ checkedAt: 1, models: [{ ...model, api: "typesafe-system-one" }] }, "https://new.example/v1");
		assert.equal(cached?.models[0].api, "openai-decisions");
		assert.equal(cached?.models[0].baseUrl, "https://new.example/v1");
		assert.equal(toExplabsModel({ ...row.model, slug: "jev-latest" }, row.providers.map(p => ({ ...p, provider: "typesafe" })), "https://gateway.example/v1")?.api, "typesafe-system-one");
		assert.equal(toExplabsModel({ ...row.model, slug: "gpt-6-luna", supported_params: { tools: true } }, [], "https://gateway.example/v1")?.api, "openai-responses");
	});

	it("selects classifier protocols from ordered route metadata regardless of model name", () => {
		const row = parseCatalogPage({ models: [lunaRow] }).models[0];
		const providers = [
			{ ...row.providers[0], id: "system", provider: "experiential_cloud" },
			{ ...row.providers[0], id: "decisions", provider: "openai" },
		];
		const alias = { ...row.model, slug: "custom-reviewer" };
		const model = toExplabsModel(alias, providers, "https://gateway.example/v1", ["decisions", "system"]);
		assert.equal(model?.api, "openai-decisions");
		assert.equal(toExplabsModel(alias, providers, "https://gateway.example/v1", ["system", "decisions"])?.api, "typesafe-system-one");
		assert.equal(toExplabsModel(row.model, providers, "https://gateway.example/v1", ["system"])?.api, "typesafe-system-one");
		assert.ok(model?.type === "classifier");
		const cached = rebaseCachedExplabsModels(JSON.parse(JSON.stringify({ checkedAt: 1, models: [{ ...model, api: "typesafe-system-one" }] })), "https://new.example/v1");
		assert.equal(cached?.models[0].api, "openai-decisions");
		const { protocolHints: _hints, ...legacy } = model;
		const unchanged = rebaseCachedExplabsModels({ checkedAt: 1, models: [{ ...legacy, api: "typesafe-system-one" }] }, "https://new.example/v1");
		assert.equal(unchanged?.models[0].api, "typesafe-system-one");
	});

	it("uses the Decisions wire shape, inline images, and answer names for all question types", async t => {
		const { provider, model } = await registeredProvider(t);
		const context: ClassifierContext = {
			state: { change: "Reject empty names", tests: "Unit tests pass; integration tests missing" },
			images: [{ type: "image", mimeType: "image/png", data: "aW1hZ2U=" }],
			questions: {
				review: { type: "choice", instructions: "Review evidence", criteria: { accept: "Sufficient evidence", review: "Missing evidence" } },
				ready: { type: "bool", instructions: "Ready?", criteria: { true: "Ready", false: "Needs review" } },
				risk: { type: "score", instructions: "Risk", criteria: ["Low", "High"] },
			},
		};
		const result = await provider.classify!(model, context, { apiKey: "xpl_test", fetch: async (input, init) => {
			assert.equal(String(input), `${model.baseUrl}/decisions`);
			assert.equal(new Headers(init?.headers).get("authorization"), "Bearer xpl_test");
			assert.match(new Headers(init?.headers).get("user-agent") ?? "", /^pi-experientiallabs\//);
			assert.deepEqual(JSON.parse(String(init?.body)), {
				model: "gpt-6-luna-decisions",
				input: [{ role: "user", content: [{ type: "input_text", text: JSON.stringify(context.state) }, { type: "input_image", image_url: "data:image/png;base64,aW1hZ2U=" }] }],
				questions: [
					{ type: "choice", name: "review", instructions: "Review evidence", choices: [{ value: "accept", description: "Sufficient evidence" }, { value: "review", description: "Missing evidence" }] },
					{ type: "predicate", name: "ready", instructions: "Ready?\n\nTrue means: Ready\nFalse means: Needs review" },
					{ type: "score", name: "risk", instructions: "Risk", levels: [{ label: "Low" }, { label: "High" }] },
				],
			});
			// Responses are matched by name, not by array position.
			return Response.json({ answers: [
				{ type: "score", name: "risk", score: 0.7, confidence: 0.9 },
				{ type: "predicate", name: "ready", probability: 0.1 },
				{ type: "choice", name: "review", choice: "review", confidence: 0.9, probabilities: [{ value: "accept", probability: 0.1 }, { value: "review", probability: 0.9 }] },
			], usage: { input_tokens: 100, output_tokens: 0 } });
		} });
		assert.equal(result.stopReason, "stop");
		assert.equal(result.provider, "explabs");
		assert.equal(result.model, "gpt-6-luna-decisions");
		assert.deepEqual(result.answers, {
			review: { type: "choice", choice: "review", confidence: 0.9, probabilities: { accept: 0.1, review: 0.9 } },
			ready: { type: "bool", probability: 0.1 }, risk: { type: "score", score: 0.7, confidence: 0.9 },
		});
		assert.ok(Math.abs((result.usage?.cost.total ?? NaN) - 0.00001) < 1e-15);
	});

	it("honors a caller's retry allowance for transient failures", async t => {
		const { provider, model } = await registeredProvider(t);
		let calls = 0;
		const result = await provider.classify!(model, {
			state: { text: "hello" },
			questions: { greeting: { type: "bool", instructions: "Greeting?", criteria: { true: "Yes", false: "No" } } },
		}, {
			apiKey: "xpl_test", maxRetries: 1, fetch: async () => {
				if (++calls === 1) {
					return Response.json({ error: { message: "Unavailable" } }, { status: 429, headers: { "retry-after": "0" } });
				}
				return Response.json({ answers: [{ type: "predicate", name: "greeting", probability: 1 }] });
			},
		});
		assert.equal(calls, 2);
		assert.equal(result.stopReason, "stop");
		assert.deepEqual(result.answers, { greeting: { type: "bool", probability: 1 } });
	});

	for (const failure of [429, 500, 504, "network", "timeout"] as const) {
		it(`does not retry ${failure} when the caller disables retries`, async t => {
			const { provider, model } = await registeredProvider(t);
			let calls = 0;
			const result = await provider.classify!(model, { state: { text: "hello" }, questions: { greeting: { type: "bool", instructions: "Greeting?", criteria: { true: "Yes", false: "No" } } } }, {
				apiKey: "xpl_test", maxRetries: 0, fetch: async () => {
					calls++;
					if (failure === "network") throw new TypeError("Connection lost");
					if (failure === "timeout") throw new DOMException("Timed out", "TimeoutError");
					return Response.json({ error: { message: "Unavailable" } }, { status: failure });
				},
			});
			assert.equal(calls, 1);
			assert.equal(result.stopReason, "error");
			assert.deepEqual(result.answers, {});
			assert.equal(result.usage, undefined);
		});
	}
});
