/**
 * Offline checks on the extension: what it registers, with no network and no Prime Agent.
 *
 *   npm test
 *
 * The live claims (membership, prices, streaming, cache accounting) are `verify.ts`; this
 * covers the derivation and the key precedence, where a silent mistake would be invisible.
 */

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { OpenAICompletionsCompat } from "@earendil-works/pi-ai";
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
import { type ModelFacts, toProviderModel } from "./catalog.js";
import { API_KEY_CONFIG, ENV_VAR, resolveKey, storedKey, storedKeyFrom } from "./hyper.js";
import hyper from "./index.js";

const here = dirname(fileURLToPath(import.meta.url));
const catalog = JSON.parse(readFileSync(join(here, "catalog.json"), "utf-8")) as { models: ModelFacts[] };

// A provider entry is one credential or a list of them; both shapes must resolve.
assert.equal(storedKeyFrom({ hyper: { type: "api_key", key: "k1" } }), "k1");
assert.equal(
	storedKeyFrom({ hyper: [{ type: "oauth", access: "a" }, { type: "api_key", key: "k2" }] }),
	"k2",
	"a subscription entry must not hide the API key beside it",
);
assert.equal(storedKeyFrom({ hyper: [{ type: "oauth", access: "a" }] }), undefined);
assert.equal(storedKeyFrom({}), undefined);
assert.equal(storedKeyFrom(null), undefined);

process.env.PI_OFFLINE = "1";
assert.equal(
	API_KEY_CONFIG,
	`!printf %s "$${ENV_VAR}"`,
	"the config fallback must read the env var and resolve to nothing when it is unset",
);
delete process.env[ENV_VAR];
assert.equal(resolveKey(), storedKey(), "without HYPER_API_KEY the stored credential is the source");
assert.ok(storedKey() === undefined || !storedKey()?.includes(" "), "a stored key is a single token");
assert.ok(
	storedKey() === undefined || resolveKey() === storedKey(),
	"nothing but auth.json or the env var may supply a key",
);
process.env[ENV_VAR] = "sk-hyper-test";
assert.equal(resolveKey(), "sk-hyper-test", "HYPER_API_KEY must win over the stored credential");

const registrations: Array<{ name: string; config: ProviderConfig }> = [];
const commands: string[] = [];
await hyper({
	registerProvider: (name: string, config: ProviderConfig) => {
		registrations.push({ name, config });
	},
	registerCommand: (name: string) => {
		commands.push(name);
	},
} as unknown as ExtensionAPI);

assert.equal(registrations.length, 1, "the extension must register exactly one provider");
const provider = registrations[0] as { name: string; config: ProviderConfig };
assert.equal(provider.name, "hyper");
assert.equal(provider.config.name, "Charm Hyper");
assert.equal(provider.config.baseUrl, "https://hyper.charm.land/v1");
assert.equal(provider.config.api, "openai-completions");
assert.equal(provider.config.models?.length, catalog.models.length, "every catalog model must be registered");
assert.ok(provider.config.oauth, "subscription login must be offered");
assert.equal(typeof provider.config.oauth?.login, "function");
assert.equal(typeof provider.config.oauth?.refreshToken, "function");
assert.deepEqual(commands, ["hyper"]);

const ids = (provider.config.models ?? []).map((model) => model.id);
assert.deepEqual(ids, catalog.models.map((model) => model.id), "registered models must follow the catalog order");

for (const model of catalog.models) {
	const config = toProviderModel(model);
	const compat = config.compat as OpenAICompletionsCompat;
	assert.equal(compat.maxTokensField, "max_tokens", `${model.id}: Hyper takes max_tokens`);
	assert.equal(compat.supportsStore, false, `${model.id}: Hyper has no response store`);
	assert.equal(compat.thinkingFormat, "openai", `${model.id}: reasoning goes through reasoning_effort`);
	assert.ok(config.contextWindow > 0 && config.maxTokens > 0, `${model.id}: limits must be positive`);
	assert.deepEqual(config.cost, model.cost, `${model.id}: cost must be the catalog price`);
	assert.deepEqual(
		config.input,
		model.vision ? ["text", "image"] : ["text"],
		`${model.id}: image input must follow vision support`,
	);

	if (!model.reasoning) {
		assert.equal(config.reasoning, false, `${model.id}: must not claim reasoning`);
		assert.equal(config.thinkingLevelMap, undefined, `${model.id}: must not offer thinking controls`);
		assert.equal(compat.supportsReasoningEffort, false, `${model.id}: must not send reasoning_effort`);
		continue;
	}

	assert.equal(config.reasoning, true, `${model.id}: must claim reasoning`);
	assert.equal(compat.supportsReasoningEffort, true, `${model.id}: must be allowed to send reasoning_effort`);
	const on = model.levels.filter((level) => level !== "none" && level !== "off");
	const offered = Object.entries(config.thinkingLevelMap ?? {})
		.filter(([, value]) => value !== null && value !== "none" && value !== "off")
		.map(([level]) => level);
	assert.deepEqual(offered.sort(), (on.length > 0 ? on : ["max"]).sort(), `${model.id}: offered levels must be the advertised ones`);
	assert.equal(
		config.thinkingLevelMap?.off,
		model.levels.includes("none") ? "none" : null,
		`${model.id}: the off level must follow an advertised none`,
	);
}

process.stdout.write(`ok: ${ids.length} models registered, key precedence and derivation checked\n`);
