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 { test } from 'vitest';

import {
	loadEffectiveSFPreferences,
	validatePreferences,
} from "../preferences.ts";
import {
	filterModelsByProviderModelAllow,
	isProviderAllowedByLists,
} from "../preferences-models.ts";

test("provider_model_allow: provider in allow-list and model in list passes", () => {
	const models = [
		{ provider: "minimax", id: "MiniMax-M2.7" },
		{ provider: "minimax", id: "MiniMax-M2" },
	];

	const filtered = filterModelsByProviderModelAllow(models, {
		minimax: ["MiniMax-M2.7"],
	});

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

test("provider_model_allow: provider in allow-list and model not in list is filtered", () => {
	const models = [
		{ provider: "minimax", id: "MiniMax-M2" },
		{ provider: "zai", id: "glm-5" },
	];

	const filtered = filterModelsByProviderModelAllow(models, {
		minimax: ["MiniMax-M2.7"],
	});

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		["zai/glm-5"],
		"minimax/MiniMax-M2 is removed so selection can fall through to the next provider",
	);
});

test("provider_model_allow: provider absent from allow-list is unrestricted", () => {
	const models = [
		{ provider: "minimax", id: "MiniMax-M2" },
		{ provider: "zai", id: "glm-5" },
	];

	const filtered = filterModelsByProviderModelAllow(models, {
		minimax: ["MiniMax-M2.7"],
	});

	assert.ok(filtered.some((m) => m.provider === "zai" && m.id === "glm-5"));
});

test("provider_model_allow: OpenRouter defaults to free models only", () => {
	const models = [
		{ provider: "openrouter", id: "qwen/qwen3-4b:free" },
		{ provider: "openrouter", id: "z-ai/glm-5.1" },
		{ provider: "zai", id: "glm-5.1" },
	];

	const filtered = filterModelsByProviderModelAllow(models, undefined);

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		["openrouter/qwen/qwen3-4b:free", "zai/glm-5.1"],
	);
});

test("provider_model_allow: supports OpenRouter free-model suffix patterns", () => {
	const models = [
		{ provider: "openrouter", id: "qwen/qwen3-coder:free" },
		{ provider: "openrouter", id: "openai/gpt-oss-120b:free" },
		{ provider: "openrouter", id: "minimax/minimax-m2.5" },
		{ provider: "zai", id: "glm-4.6" },
	];

	const suffixFiltered = filterModelsByProviderModelAllow(models, {
		openrouter: [":free"],
	});
	assert.deepEqual(
		suffixFiltered.map((m) => `${m.provider}/${m.id}`),
		[
			"openrouter/qwen/qwen3-coder:free",
			"openrouter/openai/gpt-oss-120b:free",
			"zai/glm-4.6",
		],
	);

	const globFiltered = filterModelsByProviderModelAllow(models, {
		openrouter: ["*:free"],
	});
	assert.deepEqual(
		globFiltered.map((m) => `${m.provider}/${m.id}`),
		suffixFiltered.map((m) => `${m.provider}/${m.id}`),
	);
});

test("provider_model_allow: OpenRouter paid models stay blocked even when explicitly listed", () => {
	const models = [
		{ provider: "openrouter", id: "z-ai/glm-5.1" },
		{ provider: "openrouter", id: "qwen/qwen3-4b:free" },
	];

	const filtered = filterModelsByProviderModelAllow(models, {
		openrouter: ["z-ai/glm-5.1", "qwen/qwen3-4b:free"],
	});

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		["openrouter/qwen/qwen3-4b:free"],
	);
});

test("provider_model_allow: OpenCode defaults to free models while OpenCode Go stays unrestricted", () => {
	const models = [
		{ provider: "opencode", id: "big-pickle" },
		{ provider: "opencode", id: "gpt-5-nano" },
		{ provider: "opencode", id: "minimax-m2.5-free" },
		{ provider: "opencode", id: "nemotron-3-super-free" },
		{ provider: "opencode", id: "gpt-5.4" },
		{ provider: "opencode", id: "kimi-k2.5" },
		{ provider: "opencode-go", id: "minimax-m2.7" },
	];

	const filtered = filterModelsByProviderModelAllow(models, undefined);

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		[
			"opencode/big-pickle",
			"opencode/gpt-5-nano",
			"opencode/minimax-m2.5-free",
			"opencode/nemotron-3-super-free",
			"opencode-go/minimax-m2.7",
		],
	);
});

test("provider_model_allow: hides Xiaomi token-plan regional aliases", () => {
	const models = [
		{ provider: "xiaomi", id: "mimo-v2-pro" },
		{ provider: "xiaomi-token-plan-ams", id: "mimo-v2-pro" },
		{ provider: "xiaomi-token-plan-cn", id: "mimo-v2-pro" },
		{ provider: "xiaomi-token-plan-sgp", id: "mimo-v2-pro" },
		{ provider: "opencode-go", id: "mimo-v2-pro" },
	];

	const filtered = filterModelsByProviderModelAllow(models, undefined);

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

test("provider_model_allow: hides Claude Code from normal selection", () => {
	const models = [
		{ provider: "claude-code", id: "sonnet" },
		{ provider: "kimi-coding", id: "kimi-for-coding" },
	];

	const filtered = filterModelsByProviderModelAllow(models, undefined);

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		["kimi-coding/kimi-for-coding"],
	);
});

test("provider_model_allow: hides Mistral non-selection endpoints", () => {
	const models = [
		{ provider: "mistral", id: "mistral-large-latest" },
		{ provider: "mistral", id: "codestral-latest" },
		{ provider: "mistral", id: "mistral-embed" },
		{ provider: "mistral", id: "mistral-ocr-latest" },
		{ provider: "mistral", id: "voxtral-mini-tts-latest" },
		{ provider: "mistral", id: "ft:codestral-latest:abc" },
	];

	const filtered = filterModelsByProviderModelAllow(models, undefined);

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		[
			"mistral/mistral-large-latest",
			"mistral/codestral-latest",
		],
	);
});

test("provider_model_block: blocks matching models even when provider is otherwise unrestricted", () => {
	const models = [
		{ provider: "minimax", id: "MiniMax-M2.7" },
		{ provider: "minimax", id: "MiniMax-M2.7-highspeed" },
		{ provider: "zai", id: "glm-5.1" },
	];

	const filtered = filterModelsByProviderModelAllow(models, undefined, {
		minimax: ["MiniMax-M2.7-highspeed"],
	});

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		["minimax/MiniMax-M2.7", "zai/glm-5.1"],
	);
});

test("provider_model_block: deny wins over provider_model_allow", () => {
	const models = [
		{ provider: "minimax", id: "MiniMax-M2.7" },
		{ provider: "minimax", id: "MiniMax-M2.7-highspeed" },
	];

	const filtered = filterModelsByProviderModelAllow(
		models,
		{ minimax: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"] },
		{ minimax: ["MiniMax-M2.7"] },
	);

	assert.deepEqual(
		filtered.map((m) => `${m.provider}/${m.id}`),
		["minimax/MiniMax-M2.7-highspeed"],
	);
});

test("blocked_providers: deny wins over allowed_providers", () => {
	assert.equal(
		isProviderAllowedByLists("zai", ["zai", "minimax"], ["zai"]),
		false,
	);
	assert.equal(isProviderAllowedByLists("minimax", ["zai"], undefined), false);
	assert.equal(isProviderAllowedByLists("minimax", undefined, ["zai"]), true);
});

test("provider_model_allow: validates shape and normalizes provider IDs", () => {
	const result = validatePreferences({
		provider_model_allow: {
			MiniMax: [" MiniMax-M2.7 ", "MiniMax-M2.7-highspeed"],
		},
		provider_model_block: {
			OpenRouter: [" z-ai/glm-5.1 ", ":paid"],
		},
		blocked_providers: [" OpenRouter ", "GOOGLE"],
	});

	assert.equal(result.errors.length, 0);
	assert.deepEqual(result.preferences.provider_model_allow, {
		minimax: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"],
	});
	assert.deepEqual(result.preferences.provider_model_block, {
		openrouter: ["z-ai/glm-5.1", ":paid"],
	});
	assert.deepEqual(result.preferences.blocked_providers, [
		"openrouter",
		"google",
	]);
});

test("provider_model_allow: rejects invalid shapes", () => {
	const result = validatePreferences({
		provider_model_allow: {
			minimax: "MiniMax-M2.7",
			zai: ["glm-5", 42],
		} as any,
		provider_model_block: {
			minimax: "MiniMax-M2.7",
		} as any,
		blocked_providers: ["openrouter", 7] as any,
	});

	assert.ok(
		result.errors.some((error) =>
			error.includes("provider_model_allow.minimax"),
		),
	);
	assert.ok(
		result.errors.some((error) => error.includes("provider_model_allow.zai")),
	);
	assert.ok(
		result.errors.some((error) =>
			error.includes("provider_model_block.minimax"),
		),
	);
	assert.ok(
		result.errors.some((error) =>
			error.includes("blocked_providers must be an array of strings"),
		),
	);
	assert.equal(result.preferences.provider_model_allow, undefined);
	assert.equal(result.preferences.provider_model_block, undefined);
	assert.equal(result.preferences.blocked_providers, undefined);
});

test("provider_model_allow: project overrides global per provider", () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = mkdtempSync(
		join(tmpdir(), "sf-provider-model-allow-project-"),
	);
	const tempHome = mkdtempSync(join(tmpdir(), "sf-provider-model-allow-home-"));

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });

		writeFileSync(
			join(tempHome, "preferences.md"),
			[
				"---",
				"provider_model_allow:",
				"  minimax:",
				"    - MiniMax-M2.7",
				"  zai:",
				"    - glm-5",
				"---",
			].join("\n"),
			"utf-8",
		);

		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			[
				"---",
				"provider_model_allow:",
				"  minimax:",
				"    - MiniMax-M2.7-highspeed",
				"---",
			].join("\n"),
			"utf-8",
		);

		process.env.SF_HOME = tempHome;
		process.chdir(tempProject);

		const loaded = loadEffectiveSFPreferences();
		assert.notEqual(loaded, null);
		assert.deepEqual(loaded!.preferences.provider_model_allow, {
			minimax: ["MiniMax-M2.7-highspeed"],
			zai: ["glm-5"],
		});
	} finally {
		process.chdir(originalCwd);
		if (originalSfHome === undefined) delete process.env.SF_HOME;
		else process.env.SF_HOME = originalSfHome;
		rmSync(tempProject, { recursive: true, force: true });
		rmSync(tempHome, { recursive: true, force: true });
	}
});
