import assert from "node:assert/strict";
import { describe, it } from "node:test";
import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "@singularity-forge/pi-ai";
import type { AuthStorage } from "./auth-storage.js";
import { ModelRegistry } from "./model-registry.js";

// ── Helpers ───────────────────────────────────────────────────────────────────

function createRegistry(hasAuthFn: (provider: string) => boolean = () => false): ModelRegistry {
	const authStorage = {
		setFallbackResolver: () => {},
		onCredentialChange: () => {},
		getOAuthProviders: () => [],
		get: () => undefined,
		hasAuth: hasAuthFn,
		getApiKey: async () => undefined,
	} as unknown as AuthStorage;
	return new ModelRegistry(authStorage, undefined);
}

const noopStream = (_m: Model<Api>, _c: Context, _o?: SimpleStreamOptions): AssistantMessageEventStream =>
	({
		[Symbol.asyncIterator]() { return { next: async () => ({ value: undefined, done: true as const }) }; },
		result: () => Promise.resolve({} as any),
		push: () => {},
		end: () => {},
	}) as unknown as AssistantMessageEventStream;

function registerNone(registry: ModelRegistry, provider: string, modelId: string): void {
	registry.registerProvider(provider, {
		authMode: "none",
		baseUrl: `http://localhost/${provider}`,
		api: "openai-completions",
		streamSimple: noopStream,
		models: [{ id: modelId, name: modelId, api: "openai-completions", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384 }],
	});
}

function registerApiKey(registry: ModelRegistry, provider: string, modelId: string): void {
	registry.registerProvider(provider, {
		apiKey: "sk-test",
		baseUrl: `https://api.${provider}.com`,
		api: "openai-completions",
		streamSimple: noopStream,
		models: [{ id: modelId, name: modelId, api: "openai-completions", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384 }],
	});
}

// ── getModelsForProxy — basic ─────────────────────────────────────────────────

describe("ModelRegistry.getModelsForProxy — basic", () => {
	it("returns empty array for unknown model id", () => {
		const registry = createRegistry();
		assert.deepEqual(registry.getModelsForProxy("no-such-model"), []);
	});

	it("returns single candidate when only one provider has the model", () => {
		const registry = createRegistry();
		registerNone(registry, "zai", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		assert.equal(result.length, 1);
		assert.equal(result[0].provider, "zai");
	});

	it("returns all candidates when multiple providers share the model id", () => {
		const registry = createRegistry();
		registerNone(registry, "zai", "glm-4-air");
		registerNone(registry, "opencode", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		assert.equal(result.length, 2);
	});
});

// ── getModelsForProxy — family priority ordering ──────────────────────────────

describe("ModelRegistry.getModelsForProxy — family priority ordering", () => {
	it("GLM family: zai before opencode before opencode-go", () => {
		const registry = createRegistry();
		// Register in reverse priority order to confirm sorting
		registerNone(registry, "opencode-go", "glm-4-air");
		registerNone(registry, "opencode",    "glm-4-air");
		registerNone(registry, "zai",         "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "zai", "zai must be first for GLM");
		assert.ok(providers.indexOf("opencode") < providers.indexOf("opencode-go"), "opencode before opencode-go");
	});

	it("Kimi family: kimi-coding before opencode", () => {
		const registry = createRegistry();
		registerNone(registry, "opencode",     "kimi-k2");
		registerNone(registry, "kimi-coding",  "kimi-k2");
		const result = registry.getModelsForProxy("kimi-k2");
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "kimi-coding", "kimi-coding must lead for kimi- models");
	});

	it("MiniMax family: minimax before minimax-cn", () => {
		const registry = createRegistry();
		registerNone(registry, "minimax-cn", "MiniMax-Text-01");
		registerNone(registry, "minimax",    "MiniMax-Text-01");
		const result = registry.getModelsForProxy("MiniMax-Text-01");
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "minimax", "minimax (international) before minimax-cn");
	});

	it("Gemini family: google-gemini-cli before google before google-vertex", () => {
		const registry = createRegistry();
		registerNone(registry, "google-vertex",    "gemini-2.5-pro");
		registerNone(registry, "google",           "gemini-2.5-pro");
		registerNone(registry, "google-gemini-cli","gemini-2.5-pro");
		const result = registry.getModelsForProxy("gemini-2.5-pro");
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "google-gemini-cli", "free CLI first for Gemini");
		assert.ok(providers.indexOf("google") < providers.indexOf("google-vertex"), "paid API before vertex");
	});

	it("provider not in any family rule falls back to end of list", () => {
		const registry = createRegistry();
		registerNone(registry, "zai",     "glm-4-air");
		registerNone(registry, "unknown-aggregator", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "zai");
		assert.equal(providers[providers.length - 1], "unknown-aggregator");
	});
});

// ── getModelsForProxy — auth-ready candidates first ───────────────────────────

describe("ModelRegistry.getModelsForProxy — auth-ready candidates first", () => {
	it("provider with auth precedes same-priority provider without auth", () => {
		// zai has auth (hasAuth → true), opencode does not
		const registry = createRegistry((p) => p === "zai");
		registerApiKey(registry, "zai",      "glm-4-air");
		registerApiKey(registry, "opencode", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		const providers = result.map((m) => m.provider);
		// zai is already first by family priority AND by auth — stays first
		assert.equal(providers[0], "zai");
	});

	it("lower-priority provider with auth beats higher-priority one without auth", () => {
		// opencode has auth, zai does not
		const registry = createRegistry((p) => p === "opencode");
		registerApiKey(registry, "zai",      "glm-4-air");
		registerApiKey(registry, "opencode", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		// opencode has auth so moves to withAuth bucket (before zai which has none)
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "opencode", "auth-ready provider surfaces first regardless of family order");
	});

	it("none-auth providers are always request-ready and not demoted", () => {
		const registry = createRegistry(() => false);
		registerNone(registry, "zai",      "glm-4-air");
		registerNone(registry, "opencode", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		// Both none-auth — family order preserved
		assert.equal(result[0].provider, "zai");
	});
});

// ── getModelsForProxy — overrides ─────────────────────────────────────────────

describe("ModelRegistry.getModelsForProxy — priority overrides", () => {
	it("override by prefix replaces family priority for matching models", () => {
		const registry = createRegistry();
		registerNone(registry, "zai",      "glm-4-air");
		registerNone(registry, "opencode", "glm-4-air");
		// Override: prefer opencode first for glm- models
		const result = registry.getModelsForProxy("glm-4-air", { "glm-": ["opencode", "zai"] });
		const providers = result.map((m) => m.provider);
		assert.equal(providers[0], "opencode", "override must reorder providers");
		assert.equal(providers[1], "zai");
	});

	it("override does not affect models with non-matching prefix", () => {
		const registry = createRegistry();
		registerNone(registry, "zai",      "glm-4-air");
		registerNone(registry, "opencode", "glm-4-air");
		// Override for kimi- should not affect glm- resolution
		const result = registry.getModelsForProxy("glm-4-air", { "kimi-": ["opencode"] });
		assert.equal(result[0].provider, "zai", "family default must apply when override prefix doesn't match");
	});
});

// ── getModel (convenience wrapper) ───────────────────────────────────────────

describe("ModelRegistry.getModel — convenience wrapper", () => {
	it("returns undefined for unknown model", () => {
		const registry = createRegistry();
		assert.equal(registry.getModel("no-such-model"), undefined);
	});

	it("returns highest-priority candidate", () => {
		const registry = createRegistry();
		registerNone(registry, "opencode", "glm-4-air");
		registerNone(registry, "zai",      "glm-4-air");
		const model = registry.getModel("glm-4-air");
		assert.ok(model);
		assert.equal(model.provider, "zai");
	});
});
