/**
 * discovery-cache-pricing.test.mjs
 *
 * Tests that the model-discovery cache now carries pricing data and that
 * isModelAllowedByBuiltInProviderPolicy correctly passes zero-cost OpenRouter
 * models that don't carry a `:free` suffix.
 */
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, test } from "vitest";

import {
	getCachedModelEntries,
	getCachedModelIds,
	parseDiscoveredModels,
} from "../model-catalog-cache.js";

// Import preferences.js so that _initPrefsLoader is called and the
// circular dep lazy-loader is wired up (required by preferences-models.js).
import "../preferences.js";
import { isProviderModelAllowed } from "../preferences-models.js";

// ─── Test helpers ─────────────────────────────────────────────────────────────

const tmpDirs = [];
let originalSfHomeTop;

beforeEach(() => {
	originalSfHomeTop = process.env.SF_HOME;
});

afterEach(() => {
	while (tmpDirs.length > 0) {
		rmSync(tmpDirs.pop(), { recursive: true, force: true });
	}
	if (originalSfHomeTop === undefined) delete process.env.SF_HOME;
	else process.env.SF_HOME = originalSfHomeTop;
});

function tempBasePath() {
	const dir = mkdtempSync(join(tmpdir(), "sf-discovery-cache-test-"));
	tmpDirs.push(dir);
	// Catalog is global (~/.sf/model-catalog) — pin SF_HOME at the temp dir so
	// each test's reads/writes land in isolation.
	process.env.SF_HOME = dir;
	return dir;
}

/** Write a model-catalog cache entry directly for testing.
 * Catalog is global (~/.sf/model-catalog/) — tests must point SF_HOME at the
 * temp basePath for isolation. The caller is expected to have set
 * `process.env.SF_HOME = basePath` before invoking this helper.
 */
function writeModelCatalogCache(basePath, providerId, modelEntries) {
	const dir = join(basePath, "model-catalog");
	mkdirSync(dir, { recursive: true });
	writeFileSync(
		join(dir, `${providerId}.json`),
		JSON.stringify({
			fetchedAt: new Date().toISOString(),
			modelIds: modelEntries,
		}),
		"utf-8",
	);
}

// ─── parseDiscoveredModels tests ──────────────────────────────────────────────

describe("parseDiscoveredModels", () => {
	test("openrouter: extracts cost from pricing field", () => {
		const cfg = { providerId: "openrouter" };
		const json = {
			data: [
				{
					id: "openrouter/owl-alpha",
					pricing: {
						prompt: "0",
						completion: "0",
						input_cache_read: "0",
						input_cache_write: "0",
					},
					context_length: 128000,
				},
			],
		};
		const result = parseDiscoveredModels(cfg, json);
		assert.ok(result, "should return a result");
		assert.equal(result.length, 1);
		const entry = result[0];
		assert.equal(entry.id, "openrouter/owl-alpha");
		assert.deepEqual(entry.cost, {
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0,
		});
		assert.equal(entry.contextWindow, 128000);
	});

	test("openrouter: extracts non-zero cost correctly", () => {
		const cfg = { providerId: "openrouter" };
		const json = {
			data: [
				{
					id: "openrouter/anthropic/claude-opus-4-6",
					pricing: {
						prompt: "0.000015",
						completion: "0.000075",
						input_cache_read: "0.0000015",
						input_cache_write: "0.00001875",
					},
				},
			],
		};
		const result = parseDiscoveredModels(cfg, json);
		assert.ok(result);
		const entry = result[0];
		assert.equal(entry.cost.input, 0.000015);
		assert.equal(entry.cost.output, 0.000075);
		assert.ok(entry.cost.cacheRead > 0);
		assert.ok(entry.cost.cacheWrite > 0);
	});

	test("non-openrouter provider: returns id-only entries (no cost)", () => {
		const cfg = { providerId: "opencode" };
		const json = {
			data: [{ id: "big-pickle" }, { id: "gpt-5-nano" }],
		};
		const result = parseDiscoveredModels(cfg, json);
		assert.ok(result);
		assert.equal(result.length, 2);
		for (const entry of result) {
			assert.equal(typeof entry.id, "string");
			assert.equal(entry.cost, undefined, "non-openrouter should have no cost");
		}
	});

	test("google provider: wraps names as id-only entries", () => {
		const cfg = { type: "google", providerId: "google" };
		const json = {
			models: [
				{ name: "models/gemini-2.5-pro" },
				{ name: "models/gemini-2.5-flash" },
			],
		};
		const result = parseDiscoveredModels(cfg, json);
		assert.ok(result);
		assert.equal(result.length, 2);
		assert.equal(result[0].id, "gemini-2.5-pro");
		assert.equal(result[0].cost, undefined);
	});

	test("returns null for unrecognized response shape", () => {
		const result = parseDiscoveredModels({}, { unexpected: true });
		assert.equal(result, null);
	});
});

// ─── Cache read/write with new entry shape ────────────────────────────────────

describe("getCachedModelEntries / getCachedModelIds", () => {
	test("round-trips DiscoveredModelEntry with cost", () => {
		const basePath = tempBasePath();
		const entries = [
			{
				id: "openrouter/owl-alpha",
				cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
				contextWindow: 128000,
			},
		];
		writeModelCatalogCache(basePath, "openrouter", entries);

		const read = getCachedModelEntries(basePath, "openrouter");
		assert.equal(read.length, 1);
		assert.equal(read[0].id, "openrouter/owl-alpha");
		assert.deepEqual(read[0].cost, {
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0,
		});
		assert.equal(read[0].contextWindow, 128000);
	});

	test("getCachedModelIds returns string array from rich entries", () => {
		const basePath = tempBasePath();
		writeModelCatalogCache(basePath, "openrouter", [
			{
				id: "openrouter/owl-alpha",
				cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
			},
			{ id: "openrouter/free" },
		]);

		const ids = getCachedModelIds(basePath, "openrouter");
		assert.deepEqual(ids, ["openrouter/owl-alpha", "openrouter/free"]);
	});
});

// ─── Legacy cache migration ───────────────────────────────────────────────────

describe("legacy cache migration", () => {
	test("string[] cache migrates to {id}[] on read", () => {
		const basePath = tempBasePath();
		// Write legacy format: modelIds is a string[]
		// Global catalog dir (tempBasePath() pins SF_HOME=basePath).
		const dir = join(basePath, "model-catalog");
		mkdirSync(dir, { recursive: true });
		writeFileSync(
			join(dir, "openrouter.json"),
			JSON.stringify({
				fetchedAt: new Date().toISOString(),
				modelIds: ["openrouter/owl-alpha", "openrouter/free"],
			}),
			"utf-8",
		);

		const entries = getCachedModelEntries(basePath, "openrouter");
		assert.equal(entries.length, 2);
		assert.deepEqual(entries[0], { id: "openrouter/owl-alpha" });
		assert.deepEqual(entries[1], { id: "openrouter/free" });

		// IDs are still accessible
		const ids = getCachedModelIds(basePath, "openrouter");
		assert.deepEqual(ids, ["openrouter/owl-alpha", "openrouter/free"]);
	});
});

// ─── isProviderModelAllowed — zero-cost OpenRouter policy ─────────────────────

describe("isProviderModelAllowed — openrouter zero-cost policy", () => {
	let originalCwd;

	beforeEach(() => {
		originalCwd = process.cwd();
	});

	afterEach(() => {
		process.chdir(originalCwd);
	});

	function makeProjectWithDiscoveryCache(modelEntries) {
		// tempBasePath() pins SF_HOME at the temp dir, so the global catalog
		// writes/reads land in isolation per test.
		const basePath = tempBasePath();
		mkdirSync(join(basePath, ".sf"), { recursive: true });
		writeModelCatalogCache(basePath, "openrouter", modelEntries);
		process.chdir(basePath);
		return basePath;
	}

	test("zero-cost model without :free suffix is allowed when cost in discovery cache", () => {
		makeProjectWithDiscoveryCache([
			{
				id: "openrouter/owl-alpha",
				cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
			},
		]);

		// model object has no cost (as discovered dynamically)
		const allowed = isProviderModelAllowed(
			"openrouter",
			"openrouter/owl-alpha",
			undefined, // providerModelAllow
			undefined, // providerModelBlock
			{}, // model object without cost
		);
		assert.equal(allowed, true, "zero-cost model should be allowed");
	});

	test("paid model is blocked even when present in discovery cache", () => {
		makeProjectWithDiscoveryCache([
			{
				id: "openrouter/anthropic/claude-opus-4-6",
				cost: {
					input: 0.000015,
					output: 0.000075,
					cacheRead: 0.0000015,
					cacheWrite: 0.00001875,
				},
			},
		]);

		const allowed = isProviderModelAllowed(
			"openrouter",
			"openrouter/anthropic/claude-opus-4-6",
			undefined,
			undefined,
			{}, // model object without cost (simulates dynamic discovery)
		);
		assert.equal(allowed, false, "paid model should not pass zero-cost filter");
	});

	test("model with :free suffix is allowed regardless of discovery cache", () => {
		makeProjectWithDiscoveryCache([]); // empty cache

		const allowed = isProviderModelAllowed(
			"openrouter",
			"openrouter/qwen/qwen3-235b-a22b:free",
			undefined,
			undefined,
			{}, // no cost
		);
		assert.equal(allowed, true, ":free suffix model must always be allowed");
	});

	test("non-openrouter provider with no cost entry is allowed", () => {
		makeProjectWithDiscoveryCache([]);

		const allowed = isProviderModelAllowed(
			"anthropic",
			"claude-sonnet-4-6",
			undefined,
			undefined,
			{},
		);
		assert.equal(allowed, true, "non-openrouter provider should pass through");
	});

	test("discovery cache absent: openrouter model with no cost and no :free is blocked", () => {
		// No cache written — chdir to tmpdir with no .sf
		const emptyDir = tempBasePath();
		process.chdir(emptyDir);

		const allowed = isProviderModelAllowed(
			"openrouter",
			"openrouter/some/unknown-model",
			undefined,
			undefined,
			{}, // no cost
		);
		assert.equal(
			allowed,
			false,
			"without cache data and no :free suffix, model must be blocked",
		);
	});
});
