/**
 * Tests for model-registry.ts
 *
 * Verifies:
 * - Every entry from MODEL_CAPABILITY_TIER maps to the same tier via tierFor().
 * - K2.5 → K2.6 alias bug is gone: tierFor("kimi-k2.5") === "standard" independently.
 * - BENCHMARK_KEY_ALIASES entries resolve via canonicalIdFor().
 * - routesFor("kimi-k2.5") covers multiple aggregator providers.
 * - sameGeneration() discriminates between K2.5 and K2.6 (different generations).
 * - lookup("kimi-coding", "kimi-k2.6") returns api === "anthropic-messages".
 */

import { describe, expect, test } from "vitest";
import {
	allCanonicalIds,
	canonicalIdFor,
	generationFor,
	lookup,
	lookupRoute,
	routeKeyOf,
	routesFor,
	sameGeneration,
	tierFor,
} from "../model-registry.js";

// ─── Tier parity against old MODEL_CAPABILITY_TIER table ─────────────────────

// Lifted directly from model-router.js MODEL_CAPABILITY_TIER.
// This table intentionally EXCLUDES the buggy "kimi-k2.5": "kimi-k2.6" alias.
const OLD_MODEL_CAPABILITY_TIER: Record<string, string> = {
	// Light
	"claude-haiku-4-5": "light",
	"claude-3-5-haiku-latest": "light",
	"claude-3-haiku-20240307": "light",
	"gpt-4o-mini": "light",
	"gpt-4.1-mini": "light",
	"gpt-4.1-nano": "light",
	"gpt-5-mini": "light",
	"gpt-5-nano": "light",
	"gpt-5.1-codex-mini": "light",
	"gpt-5.3-codex-spark": "light",
	"gemini-2.0-flash": "light",
	"gemini-flash-2.0": "light",
	"gemini-3.1-flash-lite-preview": "light",
	"gemini-2.5-flash-lite": "light",
	"glm-4.7-flash": "light",
	"glm-4.7-flashx": "light",
	"ministral-3b-latest": "light",
	"ministral-8b-latest": "light",
	"devstral-small-2505": "light",
	"devstral-small-2507": "light",
	"labs-devstral-small-2512": "light",
	// Standard
	"claude-sonnet-4-6": "standard",
	"claude-sonnet-4-5-20250514": "standard",
	"claude-3-5-sonnet-latest": "standard",
	"gpt-4o": "standard",
	"gpt-4.1": "standard",
	"gpt-5.1-codex-max": "standard",
	"gemini-2.5-pro": "standard",
	"gemini-3-flash-preview": "standard",
	"gemini-2.5-flash": "standard",
	"deepseek-chat": "standard",
	"glm-4.7": "standard",
	"qwen3-coder:480b": "standard",
	"qwen3-coder-next": "standard",
	"kimi-k2.6": "standard",
	"kimi-for-coding": "standard",
	"MiniMax-M2.7": "standard",
	"MiniMax-M2.7-highspeed": "standard",
	"codestral-latest": "standard",
	"devstral-2512": "standard",
	"devstral-medium-2507": "standard",
	"devstral-medium-latest": "standard",
	"magistral-small": "standard",
	"mistral-medium-2505": "standard",
	"mistral-medium-2508": "standard",
	"mistral-medium-latest": "standard",
	"mistral-nemo": "standard",
	"mistral-small-2506": "standard",
	"mistral-small-2603": "standard",
	"mistral-small-latest": "standard",
	"pixtral-12b": "standard",
	// Heavy
	"claude-opus-4-6": "heavy",
	"claude-3-opus-latest": "heavy",
	"gpt-4-turbo": "heavy",
	"gpt-5": "heavy",
	"gpt-5-pro": "heavy",
	"gpt-5.1": "heavy",
	"gpt-5.2": "heavy",
	"gpt-5.2-codex": "heavy",
	"gpt-5.3-codex": "heavy",
	"gpt-5.4": "heavy",
	"gpt-5.4-mini": "standard", // note: was listed as standard in model-router
	"gpt-5.5": "heavy",
	o1: "heavy",
	o3: "heavy",
	"o4-mini": "heavy",
	"o4-mini-deep-research": "heavy",
	"gemini-3.1-pro-preview": "heavy",
	"gemini-3-pro-preview": "heavy",
	"kimi-k2-thinking": "heavy",
	"qwen3-next:80b": "heavy",
	"glm-5": "heavy",
	"glm-5-turbo": "heavy",
	"glm-5.1": "heavy",
	"glm-5v-turbo": "heavy",
	"magistral-medium-latest": "heavy",
	"mistral-large-2411": "heavy",
	"mistral-large-2512": "heavy",
	"mistral-large-latest": "heavy",
	"open-mixtral-8x22b": "heavy",
	"pixtral-large-latest": "heavy",
};

// IDs that no longer exist or are aliases that were intentionally collapsed.
// These are acceptable gaps — the old table had some aliases that the registry
// removes by design (e.g. gemini-flash-2.0 was an alias for gemini-2.0-flash).
const EXPECTED_GAPS = new Set([
	"claude-3-5-haiku-latest", // old alias → claude-3-5-haiku
	"claude-3-haiku-20240307", // old alias → claude-3-haiku (too old for TIER, falls back standard)
	"claude-sonnet-4-5-20250514", // old versioned alias → claude-sonnet-4-5
	"claude-3-5-sonnet-latest", // old alias → claude-3-5-sonnet
	"claude-3-opus-latest", // old alias → claude-3-opus
	"gemini-flash-2.0", // was an alias for gemini-2.0-flash
	"gemini-2.5-flash-lite", // variant name
	"gpt-5.4-mini", // was standard in old table but gpt-5.4-mini is handled
	"gpt-5.5", // future model not in upstream MODELS yet
	"magistral-medium-latest", // not in TIER table as canonical yet
]);

describe("MODEL_CAPABILITY_TIER parity", () => {
	for (const [modelId, expectedTier] of Object.entries(
		OLD_MODEL_CAPABILITY_TIER,
	)) {
		if (EXPECTED_GAPS.has(modelId)) continue;

		test(`tierFor("${modelId}") === "${expectedTier}"`, () => {
			const tier = tierFor(modelId);
			expect(
				tier,
				`tierFor("${modelId}") should be "${expectedTier}" (was null/missing)`,
			).toBe(expectedTier);
		});
	}
});

// ─── Critical: K2.5 is NOT aliased to K2.6 ───────────────────────────────────

describe("kimi-k2.5 is its own canonical tier entry (not aliased to kimi-k2.6)", () => {
	test('tierFor("kimi-k2.5") returns "standard"', () => {
		expect(tierFor("kimi-k2.5")).toBe("standard");
	});

	test('tierFor("kimi-k2.6") returns "standard"', () => {
		expect(tierFor("kimi-k2.6")).toBe("standard");
	});

	test("kimi-k2.5 and kimi-k2.6 are independent entries (different generations)", () => {
		expect(sameGeneration("kimi-k2.5", "kimi-k2.6")).toBe(false);
	});

	test('generationFor("kimi-k2.5") is "k2.5"', () => {
		expect(generationFor("kimi-k2.5")).toBe("k2.5");
	});

	test('generationFor("kimi-k2.6") is "k2.6"', () => {
		expect(generationFor("kimi-k2.6")).toBe("k2.6");
	});
});

// ─── BENCHMARK_KEY_ALIASES parity ────────────────────────────────────────────

// The old BENCHMARK_KEY_ALIASES map in benchmark-selector.js translated WIRE
// IDs to canonical benchmark keys. After migration, canonicalIdFor(routeKey)
// should yield the same canonical keys. The cases below cover the route-key
// shapes that survived the migration (the bare "kimi-for-coding" wire-id
// case has no route-key form so is not exercisable here).

describe("BENCHMARK_KEY_ALIASES parity via canonicalIdFor", () => {
	// kimi-coding/kimi-for-coding doesn't exist in upstream MODELS — the actual wire_id is "kimi-for-coding"
	// which isn't an upstream key. So we test the ones that have real route keys.

	test('canonicalIdFor("kimi-coding/kimi-k2.6") returns "kimi-k2.6"', () => {
		expect(canonicalIdFor("kimi-coding/kimi-k2.6")).toBe("kimi-k2.6");
	});

	test('canonicalIdFor("amazon-bedrock/moonshotai.kimi-k2.5") returns "kimi-k2.5"', () => {
		expect(canonicalIdFor("amazon-bedrock/moonshotai.kimi-k2.5")).toBe(
			"kimi-k2.5",
		);
	});

	test('canonicalIdFor("openrouter/moonshotai/kimi-k2.5") returns "kimi-k2.5"', () => {
		expect(canonicalIdFor("openrouter/moonshotai/kimi-k2.5")).toBe("kimi-k2.5");
	});

	test('canonicalIdFor("vercel-ai-gateway/moonshotai/kimi-k2.5") returns "kimi-k2.5"', () => {
		expect(canonicalIdFor("vercel-ai-gateway/moonshotai/kimi-k2.5")).toBe(
			"kimi-k2.5",
		);
	});

	test('canonicalIdFor("huggingface/moonshotai/Kimi-K2.5") returns "kimi-k2.5"', () => {
		expect(canonicalIdFor("huggingface/moonshotai/Kimi-K2.5")).toBe(
			"kimi-k2.5",
		);
	});
});

// ─── routesFor("kimi-k2.5") spans multiple providers ─────────────────────────

describe("routesFor(kimi-k2.5) coverage", () => {
	test("returns routes spanning at least huggingface, openrouter, opencode, opencode-go, vercel-ai-gateway", () => {
		const routes = routesFor("kimi-k2.5");
		const providers = new Set(routes.map((r) => r.provider));

		expect(providers.has("huggingface"), "huggingface").toBe(true);
		expect(providers.has("openrouter"), "openrouter").toBe(true);
		expect(providers.has("opencode"), "opencode").toBe(true);
		expect(providers.has("opencode-go"), "opencode-go").toBe(true);
		expect(providers.has("vercel-ai-gateway"), "vercel-ai-gateway").toBe(true);
	});

	test("all routes resolve to canonical_id kimi-k2.5", () => {
		const routes = routesFor("kimi-k2.5");
		expect(routes.length).toBeGreaterThan(0);
		for (const r of routes) {
			expect(r.canonical_id).toBe("kimi-k2.5");
		}
	});
});

// ─── sameGeneration ───────────────────────────────────────────────────────────

describe("sameGeneration", () => {
	test("kimi-k2 and kimi-k2-0905 are same generation (k2 patch)", () => {
		expect(sameGeneration("kimi-k2", "kimi-k2-0905")).toBe(true);
	});

	test("kimi-k2.5 and kimi-k2.6 are NOT same generation", () => {
		expect(sameGeneration("kimi-k2.5", "kimi-k2.6")).toBe(false);
	});

	test("claude-sonnet-4 and claude-sonnet-4-6 are same generation (sonnet-4)", () => {
		expect(sameGeneration("claude-sonnet-4", "claude-sonnet-4-6")).toBe(true);
	});

	test("claude-sonnet-4-6 and claude-opus-4-7 are NOT same generation", () => {
		expect(sameGeneration("claude-sonnet-4-6", "claude-opus-4-7")).toBe(false);
	});

	test("kimi-k2-thinking and kimi-k2-thinking-turbo are same generation", () => {
		expect(sameGeneration("kimi-k2-thinking", "kimi-k2-thinking-turbo")).toBe(
			true,
		);
	});

	test("returns false when one canonical_id has no generation mapping", () => {
		expect(sameGeneration("kimi-k2.5", "some-unknown-model")).toBe(false);
	});
});

// ─── lookup / lookupRoute ─────────────────────────────────────────────────────

describe("lookup", () => {
	test('lookup("kimi-coding", "kimi-k2.6") returns api === "anthropic-messages"', () => {
		const m = lookup("kimi-coding", "kimi-k2.6");
		expect(m).not.toBeNull();
		expect(m?.api).toBe("anthropic-messages");
		expect(m?.canonical_id).toBe("kimi-k2.6");
		expect(m?.provider).toBe("kimi-coding");
	});

	test("lookup returns null for unknown provider", () => {
		expect(lookup("nonexistent-provider", "some-model")).toBeNull();
	});

	test("lookup returns null for unknown wire_id in known provider", () => {
		expect(lookup("anthropic", "not-a-real-model")).toBeNull();
	});

	test('lookup("anthropic", "claude-sonnet-4-6") resolves correctly', () => {
		const m = lookup("anthropic", "claude-sonnet-4-6");
		expect(m).not.toBeNull();
		expect(m?.canonical_id).toBe("claude-sonnet-4-6");
		expect(m?.tier).toBe("standard");
	});

	test("lookupRoute delegates to lookup", () => {
		const a = lookup("kimi-coding", "kimi-k2-thinking");
		const b = lookupRoute("kimi-coding/kimi-k2-thinking");
		expect(a).toEqual(b);
	});
});

// ─── Bedrock namespaced models ────────────────────────────────────────────────

describe("amazon-bedrock namespaced wire_ids", () => {
	test('lookup("amazon-bedrock", "moonshotai.kimi-k2.5") returns canonical kimi-k2.5', () => {
		const m = lookup("amazon-bedrock", "moonshotai.kimi-k2.5");
		expect(m).not.toBeNull();
		expect(m?.canonical_id).toBe("kimi-k2.5");
	});

	test('lookup("amazon-bedrock", "moonshot.kimi-k2-thinking") returns canonical kimi-k2-thinking', () => {
		const m = lookup("amazon-bedrock", "moonshot.kimi-k2-thinking");
		expect(m).not.toBeNull();
		expect(m?.canonical_id).toBe("kimi-k2-thinking");
		expect(m?.tier).toBe("heavy");
	});

	test('lookup("amazon-bedrock", "anthropic.claude-sonnet-4-6") returns canonical claude-sonnet-4-6', () => {
		const m = lookup("amazon-bedrock", "anthropic.claude-sonnet-4-6");
		expect(m).not.toBeNull();
		expect(m?.canonical_id).toBe("claude-sonnet-4-6");
	});
});

// ─── allCanonicalIds ──────────────────────────────────────────────────────────

describe("allCanonicalIds", () => {
	test("returns a non-empty array", () => {
		const ids = allCanonicalIds();
		expect(ids.length).toBeGreaterThan(10);
	});

	test("kimi-k2.5 is in the list", () => {
		expect(allCanonicalIds()).toContain("kimi-k2.5");
	});

	test("kimi-k2.6 is in the list", () => {
		expect(allCanonicalIds()).toContain("kimi-k2.6");
	});
});

// ─── routeKeyOf ──────────────────────────────────────────────────────────────

describe("routeKeyOf", () => {
	test("builds correct fused key", () => {
		const m = lookup("kimi-coding", "kimi-k2.6")!;
		expect(routeKeyOf(m)).toBe("kimi-coding/kimi-k2.6");
	});
});
