import assert from "node:assert/strict";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, it } from "vitest";
import { AuthStorage } from "./auth-storage.js";
import { ModelDiscoveryCache } from "./discovery-cache.js";
import {
	getDefaultTTL,
	getDiscoverableCatalogSources,
	getDiscoverableProviders,
	getDiscoveryAdapter,
} from "./model-discovery.js";
import { ModelRegistry } from "./model-registry.js";

let testDir: string;

beforeEach(() => {
	testDir = join(
		tmpdir(),
		`model-registry-discovery-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
	);
	mkdirSync(testDir, { recursive: true });
});

afterEach(() => {
	try {
		rmSync(testDir, { recursive: true, force: true });
	} catch {
		// Cleanup best-effort
	}
});

// ─── discovery cache integration ─────────────────────────────────────────────

describe("ModelDiscoveryCache — integration with discovery", () => {
	it("cache respects provider-specific TTLs", () => {
		const cachePath = join(testDir, "cache.json");
		const cache = new ModelDiscoveryCache(cachePath);

		cache.set("ollama", [{ id: "llama2" }]);
		const entry = cache.get("ollama");
		assert.ok(entry);
		assert.equal(entry.ttlMs, getDefaultTTL("ollama"));
	});

	it("cache uses custom TTL when provided", () => {
		const cachePath = join(testDir, "cache.json");
		const cache = new ModelDiscoveryCache(cachePath);

		cache.set("openai", [{ id: "gpt-4o" }], 999);
		const entry = cache.get("openai");
		assert.ok(entry);
		assert.equal(entry.ttlMs, 999);
	});
});

// ─── adapter resolution ─────────────────────────────────────────────────────

describe("Discovery adapter resolution", () => {
	it("all discoverable providers have adapters", () => {
		const providers = getDiscoverableProviders();
		for (const provider of providers) {
			const adapter = getDiscoveryAdapter(provider);
			assert.equal(
				adapter.supportsDiscovery,
				true,
				`${provider} should support discovery`,
			);
			assert.notEqual(adapter.sourceType, "catalog");
		}
	});

	it("catalog sources are separate from provider discovery entries", () => {
		assert.deepEqual(getDiscoverableCatalogSources(), ["singularity-memory"]);
		assert.ok(!getDiscoverableProviders().includes("singularity-memory"));
	});

	it("static adapters return empty model lists", async () => {
		const staticProviders = [
			"anthropic",
			"openai",
			"bedrock",
			"azure-openai",
			"groq",
			"cerebras",
			"google",
		];
		for (const provider of staticProviders) {
			const adapter = getDiscoveryAdapter(provider);
			assert.equal(
				adapter.supportsDiscovery,
				false,
				`${provider} should not support discovery`,
			);
			const models = await adapter.fetchModels("dummy-key");
			assert.deepEqual(models, [], `${provider} should return empty models`);
		}
	});
});

// ─── AuthStorage hasAuth for discovery ───────────────────────────────────────

function withoutProviderEnvAuth(fn: () => void): void {
	const original = {
		OPENAI_API_KEY: process.env.OPENAI_API_KEY,
		OLLAMA_API_KEY: process.env.OLLAMA_API_KEY,
		ZAI_API_KEY: process.env.ZAI_API_KEY,
	};
	delete process.env.OPENAI_API_KEY;
	delete process.env.OLLAMA_API_KEY;
	delete process.env.ZAI_API_KEY;
	try {
		fn();
	} finally {
		for (const [key, value] of Object.entries(original)) {
			if (value === undefined) {
				delete process.env[key];
			} else {
				process.env[key] = value;
			}
		}
	}
}

describe("AuthStorage — hasAuth for discovery providers", () => {
	it("returns false for providers without auth", () => {
		withoutProviderEnvAuth(() => {
			const storage = AuthStorage.inMemory({});
			assert.equal(storage.hasAuth("openai"), false);
			assert.equal(storage.hasAuth("ollama"), false);
			assert.equal(storage.hasAuth("zai"), false);
		});
	});

	it("returns true for providers with stored keys", () => {
		withoutProviderEnvAuth(() => {
			const storage = AuthStorage.inMemory({
				openai: { type: "api_key" as const, key: "sk-test" },
				zai: { type: "api_key" as const, key: "zai-test" },
			});
			assert.equal(storage.hasAuth("openai"), true);
			assert.equal(storage.hasAuth("ollama"), false);
			assert.equal(storage.hasAuth("zai"), true);
		});
	});
});

// ─── public model-list discovery ─────────────────────────────────────────────

describe("ModelRegistry — public discovery providers", () => {
	it("discovers ollama-cloud models from live model listing without stored auth", async () => {
		const originalFetch = globalThis.fetch;
		globalThis.fetch = (async () =>
			new Response(
				JSON.stringify({
					data: [{ id: "kimi-k2.5" }, { id: "kimi-k2.6" }],
				}),
				{ status: 200 },
			)) as typeof fetch;

		try {
			const registry = new ModelRegistry(
				AuthStorage.inMemory({}),
				undefined,
				undefined,
				new ModelDiscoveryCache(join(testDir, "ollama-cloud-cache.json")),
			);
			const results = await registry.discoverModels(["ollama-cloud"]);

			assert.equal(results[0]?.provider, "ollama-cloud");
			assert.deepEqual(
				results[0]?.models.map((m) => m.id),
				["kimi-k2.5", "kimi-k2.6"],
			);
			assert.ok(
				registry
					.getAllWithDiscovered()
					.some((m) => m.provider === "ollama-cloud" && m.id === "kimi-k2.6"),
				"discovered Kimi K2.6 is retained as an ollama-cloud direct tag",
			);
		} finally {
			globalThis.fetch = originalFetch;
		}
	});

	it("makes discovered ollama-cloud models available when auth is configured", async () => {
		const originalFetch = globalThis.fetch;
		globalThis.fetch = (async () =>
			new Response(
				JSON.stringify({
					data: [{ id: "kimi-k2.6" }],
				}),
				{ status: 200 },
			)) as typeof fetch;

		try {
			const registry = new ModelRegistry(
				AuthStorage.inMemory({
					"ollama-cloud": { type: "api_key", key: "ollama-test" },
				}),
				undefined,
				undefined,
				new ModelDiscoveryCache(join(testDir, "ollama-cloud-auth-cache.json")),
			);
			await registry.discoverModels(["ollama-cloud"]);

			assert.ok(
				registry
					.getAvailable()
					.some((m) => m.provider === "ollama-cloud" && m.id === "kimi-k2.6"),
				"discovered Ollama Cloud models are available after discovery",
			);
		} finally {
			globalThis.fetch = originalFetch;
		}
	});

	it("discovers direct provider models when auth is configured", async () => {
		const originalFetch = globalThis.fetch;
		globalThis.fetch = (async () =>
			new Response(
				JSON.stringify({
					data: [
						{
							id: "glm-5.2",
							name: "GLM-5.2",
							context_length: 200000,
						},
						{
							id: "glm-4.5-air",
						},
					],
				}),
				{ status: 200 },
			)) as typeof fetch;

		try {
			const registry = new ModelRegistry(
				AuthStorage.inMemory({
					zai: { type: "api_key", key: "zai-test" },
				}),
				undefined,
				undefined,
				new ModelDiscoveryCache(join(testDir, "zai-cache.json")),
			);
			const results = await registry.discoverModels(["zai"]);

			assert.equal(results[0]?.provider, "zai");
			assert.deepEqual(
				results[0]?.models.map((m) => m.id),
				["glm-5.2", "glm-4.5-air"],
			);
			const model = registry
				.getAllWithDiscovered()
				.find((m) => m.provider === "zai" && m.id === "glm-5.2");
			assert.ok(model, "discovered direct model should be available under zai");
			assert.equal(model.api, "openai-completions");
			assert.equal(model.baseUrl, "https://api.z.ai/api/coding/paas/v4");
			const knownModel = registry
				.getDiscoveredModels()
				.find((m) => m.provider === "zai" && m.id === "glm-4.5-air");
			assert.ok(
				knownModel,
				"known direct model should be materialized from live discovery",
			);
			assert.equal(knownModel.name, "GLM-4.5-Air");
			assert.equal(knownModel.reasoning, true);
		} finally {
			globalThis.fetch = originalFetch;
		}
	});

	it("discovers Singularity Memory catalog models under their execution provider", async () => {
		const originalFetch = globalThis.fetch;
		globalThis.fetch = (async () =>
			new Response(
				JSON.stringify({
					data: [
						{
							id: "xiaomi/mimo-v9-pro",
							name: "Xiaomi MiMo V9 Pro",
							context_length: 1048576,
							top_provider: { max_completion_tokens: 131072 },
							pricing: { prompt: "0.0000004", completion: "0.000002" },
							x_singularity: {
								provider: "xiaomi",
								api: "anthropic-messages",
								base_url: "https://token-plan-ams.xiaomimimo.com/anthropic",
							},
						},
					],
				}),
				{ status: 200 },
			)) as typeof fetch;

		try {
			const registry = new ModelRegistry(
				AuthStorage.inMemory({}),
				undefined,
				undefined,
				new ModelDiscoveryCache(join(testDir, "memory-cache.json")),
			);
			const results = await registry.discoverModelCatalogs([
				"singularity-memory",
			]);

			assert.equal(results[0]?.provider, "singularity-memory");
			assert.equal(results[0]?.sourceType, "catalog");
			const model = registry
				.getAllWithDiscovered()
				.find((m) => m.provider === "xiaomi" && m.id === "mimo-v9-pro");
			assert.ok(
				model,
				"Memory-discovered model should be available under xiaomi provider",
			);
			assert.equal(model.api, "anthropic-messages");
			assert.equal(
				model.baseUrl,
				"https://token-plan-ams.xiaomimimo.com/anthropic",
			);
			assert.equal(model.contextWindow, 1048576);
			assert.equal(model.maxTokens, 131072);
			assert.deepEqual(model.cost, {
				input: 0.4,
				output: 2,
				cacheRead: 0,
				cacheWrite: 0,
			});
		} finally {
			globalThis.fetch = originalFetch;
		}
	});
});

// ─── cache persistence across instances ──────────────────────────────────────

describe("ModelDiscoveryCache — persistence", () => {
	it("data survives across cache instances", () => {
		const cachePath = join(testDir, "persist.json");

		const cache1 = new ModelDiscoveryCache(cachePath);
		cache1.set("openai", [
			{ id: "gpt-4o", name: "GPT-4o", contextWindow: 128000 },
			{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
		]);

		const cache2 = new ModelDiscoveryCache(cachePath);
		const entry = cache2.get("openai");
		assert.ok(entry);
		assert.equal(entry.models.length, 2);
		assert.equal(entry.models[0].contextWindow, 128000);
	});

	it("clear persists across instances", () => {
		const cachePath = join(testDir, "clear.json");

		const cache1 = new ModelDiscoveryCache(cachePath);
		cache1.set("openai", [{ id: "gpt-4o" }]);
		cache1.clear("openai");

		const cache2 = new ModelDiscoveryCache(cachePath);
		assert.equal(cache2.get("openai"), undefined);
	});
});

// ─── discovery TTL values ────────────────────────────────────────────────────

describe("Discovery TTL configuration", () => {
	it("live-listed providers use a one-hour TTL", () => {
		for (const provider of getDiscoverableProviders()) {
			assert.equal(
				getDefaultTTL(provider),
				60 * 60 * 1000,
				`${provider} should use the live discovery TTL`,
			);
		}
	});

	it("unknown providers get default TTL", () => {
		const customTTL = getDefaultTTL("my-custom-provider");
		const defaultTTL = getDefaultTTL("default");
		// Unknown providers should get the same TTL as the explicit "default" key
		assert.equal(customTTL, defaultTTL);
	});
});
