import assert from "node:assert/strict";
import type {
	Api,
	AssistantMessageEventStream,
	Context,
	Model,
	SimpleStreamOptions,
} from "@singularity-forge/ai";
import { describe, it } from "vitest";
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-go", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		assert.equal(result.length, 2);
	});

	it("filters paid OpenRouter and OpenCode models while keeping zero-cost and subscribed models", () => {
		const registry = createRegistry(() => true);
		registry.registerProvider("openrouter", {
			authMode: "none",
			baseUrl: "https://openrouter.ai/api/v1",
			api: "openai-completions",
			streamSimple: noopStream,
			models: [
				{
					id: "qwen/qwen3-coder:free",
					name: "Qwen Free",
					api: "openai-completions",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 128000,
					maxTokens: 16384,
				},
				{
					id: "z-ai/glm-5.1",
					name: "GLM Paid",
					api: "openai-completions",
					reasoning: false,
					input: ["text"],
					cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 128000,
					maxTokens: 16384,
				},
				{
					id: "zero-cost/non-free-slug",
					name: "Zero Cost Non-Free Slug",
					api: "openai-completions",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 128000,
					maxTokens: 16384,
				},
			],
		});
		const available = registry.getAvailable();

		assert.ok(
			available.some(
				(m) => m.provider === "openrouter" && m.id === "qwen/qwen3-coder:free",
			),
			"OpenRouter free models should remain available",
		);
		assert.ok(
			!available.some(
				(m) => m.provider === "openrouter" && m.id === "z-ai/glm-5.1",
			),
			"OpenRouter paid models should not be available",
		);
		assert.ok(
			available
				.filter((m) => m.provider === "openrouter")
				.every((m) => m.id.endsWith(":free") || m.cost?.input === 0),
			"every available OpenRouter model must use the :free SKU or carry zero-cost metadata",
		);
		assert.ok(
			available.some(
				(m) =>
					m.provider === "openrouter" && m.id === "zero-cost/non-free-slug",
			),
			"OpenRouter models with explicit zero-cost metadata should remain available",
		);
		assert.equal(
			registry.find("openrouter", "z-ai/glm-5.1"),
			undefined,
			"direct model lookup should also block paid OpenRouter models",
		);

		assert.ok(
			available.some((m) => m.provider === "opencode" && m.id === "gpt-5-nano"),
			"zero-cost OpenCode models should remain available",
		);
		assert.ok(
			available.some(
				(m) => m.provider === "opencode" && m.id === "minimax-m2.5-free",
			),
			"OpenCode models marked free should remain available",
		);
		assert.ok(
			!available.some((m) => m.provider === "opencode" && m.id === "gpt-5.4"),
			"paid OpenCode models should not be available",
		);
		assert.equal(
			registry.find("opencode", "kimi-k2.5"),
			undefined,
			"direct model lookup should also block paid OpenCode models",
		);

		assert.ok(
			available.some((m) => m.provider === "opencode-go" && m.id === "glm-5.1"),
			"OpenCode Go should expose the full subscribed model set",
		);
		assert.ok(
			available.some(
				(m) => m.provider === "opencode-go" && m.id === "minimax-m2.7",
			),
			"OpenCode Go paid-tier models should remain available",
		);
	});

	it("does not use paid OpenCode as a proxy fallback when OpenCode Go has the same model", () => {
		const registry = createRegistry(() => true);

		const result = registry.getModelsForProxy("kimi-k2.5");

		assert.ok(
			result.some((m) => m.provider === "opencode-go" && m.id === "kimi-k2.5"),
			"OpenCode Go should remain a candidate for subscribed models",
		);
		assert.ok(
			!result.some((m) => m.provider === "opencode" && m.id === "kimi-k2.5"),
			"paid OpenCode should not be a fallback candidate",
		);
	});

	it("hides explicit Xiaomi token-plan regional aliases while keeping the default Xiaomi provider", () => {
		const registry = createRegistry(() => true);
		const available = registry.getAvailable();

		assert.ok(
			available.some((m) => m.provider === "xiaomi" && m.id === "mimo-v2-pro"),
			"xiaomi/default AMS provider should remain available",
		);
		assert.ok(
			!available.some((m) => m.provider.startsWith("xiaomi-token-plan-")),
			"regional Xiaomi token-plan aliases should not be listed",
		);
		assert.equal(
			registry.find("xiaomi-token-plan-ams", "mimo-v2-pro"),
			undefined,
			"direct lookup should also hide regional Xiaomi token-plan aliases",
		);
	});

	it("routes MiMo proxy candidates through direct Xiaomi and subscribed/free relays, never OpenRouter", () => {
		const registry = createRegistry(() => true);
		registerNone(registry, "openrouter", "mimo-v2-pro");
		registerNone(registry, "ollama-cloud", "mimo-v2-pro");

		const result = registry.getModelsForProxy("mimo-v2-pro");
		assert.deepEqual(
			result.map((m) => `${m.provider}/${m.id}`),
			[
				"xiaomi/mimo-v2-pro",
				"opencode-go/mimo-v2-pro",
				"ollama-cloud/mimo-v2-pro",
			],
		);
	});

	it("hides Grok models even when they arrive through aggregators", () => {
		const registry = createRegistry(() => true);
		const available = registry.getAvailable();

		assert.ok(
			!available.some(
				(m) =>
					m.provider === "xai" ||
					m.id.toLowerCase().includes("grok") ||
					m.id.toLowerCase().startsWith("x-ai/"),
			),
			"Grok/xAI models should not be visible through direct or aggregate providers",
		);
		assert.equal(
			registry.find("openrouter", "x-ai/grok-4:free"),
			undefined,
			"direct lookup should also block OpenRouter Grok SKUs",
		);
	});

	it("hides Groq as a selectable LLM model provider", () => {
		const registry = createRegistry(() => true);
		const available = registry.getAvailable();

		assert.ok(
			!available.some((m) => m.provider === "groq"),
			"Groq should not be listed or selected by SF provider policy",
		);
		assert.equal(
			registry.find("groq", "llama-3.1-8b-instant"),
			undefined,
			"direct lookup should also hide Groq models",
		);
	});

	it("hides Claude Code because it is not part of the managed provider pool", () => {
		const registry = createRegistry(() => true);
		const available = registry.getAvailable();

		assert.ok(
			!available.some((m) => m.provider === "claude-code"),
			"Claude Code should not be listed or selected by SF provider policy",
		);
		assert.equal(
			registry.find("claude-code", "sonnet"),
			undefined,
			"direct lookup should also hide Claude Code models",
		);
	});

	it("hides Mistral non-selection endpoints while keeping chat and coding models", () => {
		const registry = createRegistry(() => true);
		registry.registerProvider("mistral", {
			authMode: "none",
			baseUrl: "https://api.mistral.ai",
			api: "mistral-conversations",
			streamSimple: noopStream,
			models: [
				{
					id: "mistral-large-latest",
					name: "Mistral Large",
					api: "mistral-conversations",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 128000,
					maxTokens: 16384,
				},
				{
					id: "codestral-latest",
					name: "Codestral",
					api: "mistral-conversations",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 128000,
					maxTokens: 16384,
				},
				{
					id: "mistral-embed",
					name: "Mistral Embed",
					api: "mistral-conversations",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 8192,
					maxTokens: 8192,
				},
				{
					id: "mistral-ocr-latest",
					name: "Mistral OCR",
					api: "mistral-conversations",
					reasoning: false,
					input: ["text", "image"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 8192,
					maxTokens: 8192,
				},
				{
					id: "voxtral-mini-tts-latest",
					name: "Voxtral TTS",
					api: "mistral-conversations",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 8192,
					maxTokens: 8192,
				},
				{
					id: "ft:codestral-latest:abc",
					name: "Private Fine Tune",
					api: "mistral-conversations",
					reasoning: false,
					input: ["text"],
					cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
					contextWindow: 8192,
					maxTokens: 8192,
				},
			],
		});

		const available = registry
			.getAvailable()
			.filter((m) => m.provider === "mistral")
			.map((m) => m.id);

		assert.deepEqual(available, ["mistral-large-latest", "codestral-latest"]);
	});
});

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

describe("ModelRegistry.getModelsForProxy — family priority ordering", () => {
	it("GLM family: zai before subscribed/free relays, never OpenRouter", () => {
		const registry = createRegistry();
		// Register in reverse priority order to confirm sorting
		registerNone(registry, "openrouter", "glm-4-air");
		registerNone(registry, "ollama-cloud", "glm-4-air");
		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.deepEqual(providers, ["zai", "opencode-go", "ollama-cloud"]);
	});

	it("Kimi family: kimi-coding before subscribed/free relays, never OpenRouter", () => {
		const registry = createRegistry();
		registerNone(registry, "openrouter", "kimi-k2");
		registerNone(registry, "opencode", "kimi-k2");
		registerNone(registry, "opencode-go", "kimi-k2");
		registerNone(registry, "ollama-cloud", "kimi-k2");
		registerNone(registry, "kimi-coding", "kimi-k2");
		const result = registry.getModelsForProxy("kimi-k2");
		const providers = result.map((m) => m.provider);
		assert.deepEqual(providers, ["kimi-coding", "ollama-cloud", "opencode-go"]);
	});

	it("MiniMax family: direct providers before subscribed/free relays, never OpenRouter", () => {
		const registry = createRegistry();
		registerNone(registry, "openrouter", "MiniMax-Text-01");
		registerNone(registry, "ollama-cloud", "MiniMax-Text-01");
		registerNone(registry, "opencode-go", "MiniMax-Text-01");
		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.deepEqual(providers, ["minimax", "opencode-go", "ollama-cloud"]);
	});

	it("Gemini family: google-gemini-cli only for bare model routing", () => {
		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.deepEqual(providers, ["google-gemini-cli"]);
	});

	it("provider not in any family rule falls back to end of list", () => {
		const registry = createRegistry();
		registerNone(registry, "zai", "unknown-model");
		registerNone(registry, "unknown-aggregator", "unknown-model");
		const result = registry.getModelsForProxy("unknown-model");
		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-go does not
		const registry = createRegistry((p) => p === "zai");
		registerApiKey(registry, "zai", "glm-4-air");
		registerApiKey(registry, "opencode-go", "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-go has auth, zai does not
		const registry = createRegistry((p) => p === "opencode-go");
		registerApiKey(registry, "zai", "glm-4-air");
		registerApiKey(registry, "opencode-go", "glm-4-air");
		const result = registry.getModelsForProxy("glm-4-air");
		// opencode-go has auth so moves to withAuth bucket (before zai which has none)
		const providers = result.map((m) => m.provider);
		assert.equal(
			providers[0],
			"opencode-go",
			"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-go", "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");
	});
});

// ── provider_model_allow final filter ─────────────────────────────────────────

describe("ModelRegistry provider_model_allow filter", () => {
	it("keeps an allowed provider/model candidate", () => {
		const registry = createRegistry();
		registerNone(registry, "minimax", "MiniMax-M2.7");

		const result = registry.getModelsForProxy(
			"MiniMax-M2.7",
			{},
			{
				minimax: ["MiniMax-M2.7"],
			},
		);

		assert.ok(
			result.some(
				(model) => model.provider === "minimax" && model.id === "MiniMax-M2.7",
			),
			"allowed minimax/MiniMax-M2.7 candidate should survive filtering",
		);
	});

	it("filters a disallowed provider/model candidate and falls through", () => {
		const registry = createRegistry();
		registerNone(registry, "minimax", "MiniMax-M2");
		registerNone(registry, "minimax-cn", "MiniMax-M2");
		registerNone(registry, "opencode-go", "MiniMax-M2");

		const result = registry.getModelsForProxy(
			"MiniMax-M2",
			{},
			{
				minimax: ["MiniMax-M2.7"],
			},
		);

		assert.deepEqual(
			result.map((m) => `${m.provider}/${m.id}`),
			["opencode-go/MiniMax-M2"],
		);
	});

	it("leaves providers absent from the allow-list unrestricted", () => {
		const registry = createRegistry();
		registerNone(registry, "zai", "glm-4-air");

		const result = registry.getModelsForProxy(
			"glm-4-air",
			{},
			{
				minimax: ["MiniMax-M2.7"],
			},
		);

		assert.deepEqual(
			result.map((m) => `${m.provider}/${m.id}`),
			["zai/glm-4-air"],
		);
	});
});
