import assert from "node:assert/strict";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, it } from "vitest";
import { ModelDiscoveryCache } from "./discovery-cache.js";

let testDir: string;
let cachePath: string;

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

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

// ─── basic operations ────────────────────────────────────────────────────────

describe("ModelDiscoveryCache — basic operations", () => {
	it("starts with no entries", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		assert.equal(cache.get("openai"), undefined);
	});

	it("stores and retrieves models", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		const models = [{ id: "gpt-4o", name: "GPT-4o" }];
		cache.set("openai", models);

		const entry = cache.get("openai");
		assert.ok(entry);
		assert.deepEqual(entry.models, models);
		assert.ok(entry.fetchedAt > 0);
		assert.ok(entry.ttlMs > 0);
	});

	it("persists to disk and reloads", () => {
		const cache1 = new ModelDiscoveryCache(cachePath);
		cache1.set("openai", [{ id: "gpt-4o" }]);

		const cache2 = new ModelDiscoveryCache(cachePath);
		const entry = cache2.get("openai");
		assert.ok(entry);
		assert.equal(entry.models[0].id, "gpt-4o");
	});

	it("clear removes a specific provider", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }]);
		cache.set("google", [{ id: "gemini-pro" }]);

		cache.clear("openai");
		assert.equal(cache.get("openai"), undefined);
		const googleEntry = cache.get("google");
		assert.ok(googleEntry);
		assert.equal(googleEntry.models[0].id, "gemini-pro");
	});

	it("clear without provider removes all entries", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }]);
		cache.set("google", [{ id: "gemini-pro" }]);

		cache.clear();
		assert.equal(cache.get("openai"), undefined);
		assert.equal(cache.get("google"), undefined);
	});
});

// ─── staleness ───────────────────────────────────────────────────────────────

describe("ModelDiscoveryCache — staleness", () => {
	it("newly set entries are not stale", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }]);
		assert.equal(cache.isStale("openai"), false);
	});

	it("missing providers are stale", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		assert.equal(cache.isStale("unknown"), true);
	});

	it("entries with expired TTL are stale", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }], 1); // 1ms TTL

		// Wait for TTL to expire
		const start = Date.now();
		while (Date.now() - start < 5) {
			// busy wait
		}

		assert.equal(cache.isStale("openai"), true);
	});
});

// ─── getAll ──────────────────────────────────────────────────────────────────

describe("ModelDiscoveryCache — getAll", () => {
	it("returns non-stale entries by default", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }]);
		cache.set("stale", [{ id: "old" }], 1);

		// Wait for stale TTL
		const start = Date.now();
		while (Date.now() - start < 5) {
			// busy wait
		}

		const all = cache.getAll();
		assert.ok(all.has("openai"));
		assert.ok(!all.has("stale"));
	});

	it("returns all entries when includeStale is true", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }]);
		cache.set("stale", [{ id: "old" }], 1);

		// Wait for stale TTL
		const start = Date.now();
		while (Date.now() - start < 5) {
			// busy wait
		}

		const all = cache.getAll(true);
		assert.ok(all.has("openai"));
		assert.ok(all.has("stale"));
	});
});

// ─── edge cases ──────────────────────────────────────────────────────────────

describe("ModelDiscoveryCache — edge cases", () => {
	it("handles corrupted cache file gracefully", () => {
		writeFileSync(cachePath, "not valid json", "utf-8");
		const cache = new ModelDiscoveryCache(cachePath);
		assert.equal(cache.get("openai"), undefined);
	});

	it("handles wrong version gracefully", () => {
		writeFileSync(
			cachePath,
			JSON.stringify({ version: 99, entries: {} }),
			"utf-8",
		);
		const cache = new ModelDiscoveryCache(cachePath);
		assert.equal(cache.get("openai"), undefined);
	});

	it("handles missing cache file", () => {
		const cache = new ModelDiscoveryCache(
			join(testDir, "nonexistent", "cache.json"),
		);
		assert.equal(cache.get("openai"), undefined);
	});

	it("overwrites existing entry for same provider", () => {
		const cache = new ModelDiscoveryCache(cachePath);
		cache.set("openai", [{ id: "gpt-4o" }]);
		cache.set("openai", [{ id: "gpt-4o-mini" }]);

		const entry = cache.get("openai");
		assert.ok(entry);
		assert.equal(entry.models.length, 1);
		assert.equal(entry.models[0].id, "gpt-4o-mini");
	});
});
