import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test } from 'vitest';
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

import {
	resolveModelId,
	resolvePreferredModelConfig,
	selectAndApplyModel,
} from "../auto-model-selection.js";

function makeTempDir(prefix: string): string {
	return mkdtempSync(join(tmpdir(), prefix));
}

test("resolvePreferredModelConfig synthesizes heavy routing ceiling when models section is absent", () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = makeTempDir("sf-routing-project-");
	const tempSfHome = makeTempDir("sf-routing-home-");

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });
		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			[
				"---",
				"dynamic_routing:",
				"  enabled: true",
				"  tier_models:",
				"    light: claude-haiku-4-5",
				"    standard: claude-sonnet-4-6",
				"    heavy: claude-opus-4-6",
				"---",
			].join("\n"),
			"utf-8",
		);
		process.env.SF_HOME = tempSfHome;
		process.chdir(tempProject);

		const config = resolvePreferredModelConfig("plan-slice", {
			provider: "anthropic",
			id: "claude-sonnet-4-6",
		});

		assert.deepEqual(config, {
			primary: "claude-opus-4-6",
			fallbacks: [],
		});
	} 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(tempSfHome, { recursive: true, force: true });
	}
});

test("resolvePreferredModelConfig falls back to auto start model when heavy tier is absent", () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = makeTempDir("sf-routing-project-");
	const tempSfHome = makeTempDir("sf-routing-home-");

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });
		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			[
				"---",
				"dynamic_routing:",
				"  enabled: true",
				"  tier_models:",
				"    light: claude-haiku-4-5",
				"    standard: claude-sonnet-4-6",
				"---",
			].join("\n"),
			"utf-8",
		);
		process.env.SF_HOME = tempSfHome;
		process.chdir(tempProject);

		const config = resolvePreferredModelConfig("execute-task", {
			provider: "openai",
			id: "gpt-5.4",
		});

		assert.deepEqual(config, {
			primary: "openai/gpt-5.4",
			fallbacks: [],
		});
	} 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(tempSfHome, { recursive: true, force: true });
	}
});

test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = makeTempDir("sf-routing-project-");
	const tempSfHome = makeTempDir("sf-routing-home-");

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });
		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			[
				"---",
				"models:",
				"  planning: claude-sonnet-4-6",
				"dynamic_routing:",
				"  enabled: true",
				"  tier_models:",
				"    heavy: claude-opus-4-6",
				"---",
			].join("\n"),
			"utf-8",
		);
		process.env.SF_HOME = tempSfHome;
		process.chdir(tempProject);

		const config = resolvePreferredModelConfig("plan-slice", {
			provider: "anthropic",
			id: "claude-opus-4-6",
		});

		assert.deepEqual(config, {
			primary: "claude-sonnet-4-6",
			fallbacks: [],
		});
	} 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(tempSfHome, { recursive: true, force: true });
	}
});

test("selectAndApplyModel does not let learned routing override an explicit execution model", async () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = makeTempDir("sf-routing-project-");
	const tempSfHome = makeTempDir("sf-routing-home-");

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });
		writeFileSync(
			join(tempSfHome, "preferences.md"),
			[
				"---",
				"version: 1",
				"models:",
				"  execution: kimi-coding/kimi-for-coding",
				"---",
			].join("\n"),
			"utf-8",
		);
		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			["---", "version: 1", "models: {}", "---"].join("\n"),
			"utf-8",
		);
		process.env.SF_HOME = tempSfHome;
		process.chdir(tempProject);

		const availableModels = [
			{
				id: "kimi-for-coding",
				provider: "kimi-coding",
				api: "openai-responses",
			},
			{ id: "glm-4.5", provider: "zai", api: "openai-responses" },
		] as any[];
		const selectedModels: any[] = [];
		let hookCalls = 0;

		const ctx = {
			sessionManager: { getSessionId: () => "test-session" },
			modelRegistry: { getAvailable: () => availableModels },
			model: { id: "glm-4.5", provider: "zai" },
			ui: { notify: () => undefined },
		} as any;
		const pi = {
			getActiveTools: () => [],
			setActiveTools: () => undefined,
			setModel: async (model: any) => {
				selectedModels.push(model);
				return true;
			},
			emitBeforeModelSelect: async () => {
				hookCalls += 1;
				return { modelId: "glm-4.5" };
			},
			emitAdjustToolSet: async () => undefined,
		} as any;

		const result = await selectAndApplyModel(
			ctx,
			pi,
			"execute-task",
			"M999/S01/T01",
			tempProject,
			{
				advisor_allowed_providers: ["anthropic"],
			},
			true,
			null,
		);

		assert.equal(hookCalls, 0, "learned routing must not override a user pin");
		assert.equal(selectedModels[0]?.provider, "kimi-coding");
		assert.equal(selectedModels[0]?.id, "kimi-for-coding");
		assert.equal(result.appliedModel?.provider, "kimi-coding");
		assert.equal(result.appliedModel?.id, "kimi-for-coding");
	} 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(tempSfHome, { recursive: true, force: true });
	}
});

// ─── resolveModelId tests ─────────────────────────────────────────────────

test("resolveModelId: bare ID resolves to claude-code when session is claude-code (#3772)", () => {
	const availableModels = [
		{ id: "claude-sonnet-4-6", provider: "anthropic" },
		{ id: "claude-sonnet-4-6", provider: "claude-code" },
	];

	// When currentProvider is "claude-code" (set by startup migration for subscription
	// users), bare IDs must resolve to claude-code to avoid the third-party block (#3772).
	const result = resolveModelId(
		"claude-sonnet-4-6",
		availableModels,
		"claude-code",
	);
	assert.ok(result, "should resolve a model");
	assert.equal(
		result.provider,
		"claude-code",
		"bare ID must resolve to claude-code when session provider is claude-code",
	);
});

test("resolveModelId: bare ID still prefers current provider when it is a first-class API provider", () => {
	const availableModels = [
		{ id: "claude-sonnet-4-6", provider: "anthropic" },
		{ id: "claude-sonnet-4-6", provider: "bedrock" },
	];

	const result = resolveModelId(
		"claude-sonnet-4-6",
		availableModels,
		"bedrock",
	);
	assert.ok(result, "should resolve a model");
	assert.equal(
		result.provider,
		"bedrock",
		"bare ID should prefer current provider when it is a real API provider",
	);
});

test("resolveModelId: explicit provider/model format still resolves to claude-code when specified", () => {
	const availableModels = [
		{ id: "claude-sonnet-4-6", provider: "anthropic" },
		{ id: "claude-sonnet-4-6", provider: "claude-code" },
	];

	const result = resolveModelId(
		"claude-code/claude-sonnet-4-6",
		availableModels,
		"anthropic",
	);
	assert.ok(result, "should resolve a model");
	assert.equal(
		result.provider,
		"claude-code",
		"explicit provider prefix must be respected",
	);
});

test("resolveModelId: bare ID with only one provider works normally", () => {
	const availableModels = [{ id: "claude-sonnet-4-6", provider: "anthropic" }];

	const result = resolveModelId(
		"claude-sonnet-4-6",
		availableModels,
		"anthropic",
	);
	assert.ok(result, "should resolve a model");
	assert.equal(result.provider, "anthropic");
});

test("resolveModelId: bare ID with claude-code as only provider still resolves", () => {
	const availableModels = [
		{ id: "claude-sonnet-4-6", provider: "claude-code" },
	];

	// If claude-code is the ONLY provider for this model, it should still resolve
	const result = resolveModelId(
		"claude-sonnet-4-6",
		availableModels,
		"claude-code",
	);
	assert.ok(result, "should resolve even when only available via claude-code");
	assert.equal(result.provider, "claude-code");
});

test("resolveModelId: bare Gemini ID prefers cli-core backend over direct google api", () => {
	const availableModels = [
		{ id: "gemini-2.5-pro", provider: "google-gemini-cli" },
		{ id: "gemini-2.5-pro", provider: "google" },
	];

	const result = resolveModelId(
		"gemini-2.5-pro",
		availableModels,
		"google-gemini-cli",
	);
	assert.ok(result, "should resolve a Gemini model");
	assert.equal(
		result.provider,
		"google-gemini-cli",
		"bare Gemini IDs should use CLI-core when api and cli backends coexist",
	);
});

test("resolveModelId: bare Gemini ID preserves google-vertex when that is the current provider", () => {
	const availableModels = [
		{ id: "gemini-2.5-pro", provider: "google" },
		{ id: "gemini-2.5-pro", provider: "google-vertex" },
	];

	const result = resolveModelId(
		"gemini-2.5-pro",
		availableModels,
		"google-vertex",
	);
	assert.ok(result, "should resolve a Gemini model");
	assert.equal(
		result.provider,
		"google-vertex",
		"vertex should remain an explicit enterprise route",
	);
});

test("resolveModelId: bare Gemini ID falls back to direct google api when cli-core backend is absent", () => {
	const availableModels = [{ id: "gemini-2.5-pro", provider: "google" }];

	const result = resolveModelId(
		"gemini-2.5-pro",
		availableModels,
		"google-gemini-cli",
	);
	assert.ok(result, "should resolve a Gemini model");
	assert.equal(result.provider, "google");
});

test("resolveModelId: bare GLM IDs prefer zai over aggregator providers", () => {
	for (const id of ["glm-5", "glm-5-turbo", "glm-5.1"]) {
		const availableModels = [
			{ id, provider: "opencode-go" },
			{ id, provider: "zai" },
		];

		const result = resolveModelId(id, availableModels, "opencode-go");
		assert.ok(result, `should resolve ${id}`);
		assert.equal(
			result.provider,
			"zai",
			`bare ${id} should use the canonical zai provider when available`,
		);
	}
});

test("resolveModelId: bare GLM IDs fall back when zai lacks that exact model", () => {
	const availableModels = [{ id: "glm-5.2-preview", provider: "opencode-go" }];

	const result = resolveModelId(
		"glm-5.2-preview",
		availableModels,
		"opencode-go",
	);
	assert.ok(result, "should resolve the available GLM model");
	assert.equal(result.provider, "opencode-go");
});

test("resolveModelId: bare Kimi K2.6 IDs prefer canonical Kimi Code over aggregators", () => {
	const availableModels = [
		{ id: "kimi-k2.6", provider: "ollama-cloud" },
		{ id: "kimi-for-coding", provider: "kimi-coding" },
		{ id: "kimi-k2.6", provider: "opencode-go" },
	];

	const result = resolveModelId("kimi-k2.6", availableModels, "ollama");
	assert.ok(result, "should resolve a Kimi model");
	assert.equal(result.provider, "kimi-coding");
	assert.equal(result.id, "kimi-for-coding");
});

test("resolveModelId: bare Kimi K2.6 can resolve Ollama Cloud exact version when present", () => {
	const availableModels = [
		{ id: "kimi-k2.5", provider: "ollama-cloud" },
		{ id: "kimi-k2.6", provider: "ollama-cloud" },
	];

	const result = resolveModelId("kimi-k2.6", availableModels, "ollama-cloud");
	assert.ok(result, "should resolve exact Kimi K2.6");
	assert.equal(result.provider, "ollama-cloud");
	assert.equal(result.id, "kimi-k2.6");
});

test("resolveModelId: bare Kimi K2.5 IDs do not route to Kimi Code K2.6", () => {
	const availableModels = [
		{ id: "kimi-for-coding", provider: "kimi-coding" },
		{ id: "kimi-k2.5", provider: "opencode-go" },
	];

	const result = resolveModelId("kimi-k2.5", availableModels, "opencode-go");
	assert.ok(result, "should resolve a Kimi model");
	assert.equal(result.provider, "opencode-go");
	assert.equal(result.id, "kimi-k2.5");
});

test("resolveModelId: bare Kimi K2.5 only resolves exact K2.5 aliases", () => {
	const availableModels = [
		{ id: "kimi-k2.5", provider: "opencode-go" },
		{ id: "kimi-for-coding", provider: "kimi-coding" },
	];

	const result = resolveModelId("kimi-k2.5", availableModels, "opencode-go");
	assert.ok(result, "should resolve a Kimi model");
	assert.equal(result.provider, "opencode-go");
	assert.equal(result.id, "kimi-k2.5");
});

test("resolveModelId: bare Kimi K2.5 still uses a real K2.5 provider when Kimi Code is preferred", () => {
	const availableModels = [
		{ id: "kimi-for-coding", provider: "kimi-coding" },
		{ id: "kimi-k2.5", provider: "opencode-go" },
	];

	const result = resolveModelId("kimi-k2.5", availableModels, "kimi-coding");
	assert.ok(result, "should resolve a real K2.5 model");
	assert.equal(result.provider, "opencode-go");
	assert.equal(result.id, "kimi-k2.5");
});

test("resolveModelId: explicit provider Kimi K2.5 route does not alias to K2.6", () => {
	const availableModels = [
		{ id: "kimi-for-coding", provider: "kimi-coding" },
		{ id: "kimi-k2.5", provider: "opencode-go" },
	];

	const result = resolveModelId(
		"kimi-coding/kimi-k2.5",
		availableModels,
		"kimi-coding",
	);
	assert.equal(result, undefined);
});

test("resolveModelId: bare MiniMax IDs prefer minimax over minimax-cn and aggregators", () => {
	const availableModels = [
		{ id: "MiniMax-M2.7", provider: "opencode-go" },
		{ id: "MiniMax-M2.7", provider: "minimax-cn" },
		{ id: "MiniMax-M2.7", provider: "minimax" },
	];

	const result = resolveModelId("MiniMax-M2.7", availableModels, "opencode-go");
	assert.ok(result, "should resolve a MiniMax model");
	assert.equal(result.provider, "minimax");
});

test("resolveModelId: bare Xiaomi IDs prefer xiaomi direct provider", () => {
	const availableModels = [
		{ id: "mimo-v2.5-pro", provider: "opencode-go" },
		{ id: "mimo-v2.5-pro", provider: "xiaomi-token-plan-ams" },
		{ id: "mimo-v2.5-pro", provider: "xiaomi" },
	];

	const result = resolveModelId(
		"mimo-v2.5-pro",
		availableModels,
		"opencode-go",
	);
	assert.ok(result, "should resolve a Xiaomi model");
	assert.equal(result.provider, "xiaomi");
});

test("resolveModelId: bare Xiaomi IDs can use OpenCode Go exact model when direct Xiaomi is absent", () => {
	const availableModels = [
		{ id: "mimo-v2-pro", provider: "opencode-go" },
		{ id: "mimo-v2-omni", provider: "opencode-go" },
	];

	const result = resolveModelId("mimo-v2-pro", availableModels, "openrouter");
	assert.ok(result, "should resolve exact MiMo model from OpenCode Go");
	assert.equal(result.provider, "opencode-go");
	assert.equal(result.id, "mimo-v2-pro");
});

test("resolveModelId: Xiaomi MiMo V2.5 Pro does not fall back to V2", () => {
	const availableModels = [
		{ id: "mimo-v2-pro", provider: "xiaomi" },
		{ id: "mimo-v2-omni", provider: "xiaomi" },
		{ id: "xiaomi/mimo-v2-pro", provider: "openrouter" },
	];

	const result = resolveModelId(
		"mimo-v2.5-pro",
		availableModels,
		"xiaomi",
	);
	assert.equal(result, undefined);
});

test("resolveModelId: Xiaomi MiMo V2.5 non-pro does not fall back to V2", () => {
	const availableModels = [
		{ id: "mimo-v2-pro", provider: "xiaomi" },
		{ id: "mimo-v2-omni", provider: "xiaomi" },
		{ id: "mimo-v2-flash", provider: "xiaomi" },
	];

	const result = resolveModelId("mimo-v2.5", availableModels, "xiaomi");
	assert.equal(result, undefined);
});

// ─── selectAndApplyModel verbose-gating tests ──────────────────────────

test("model change notify in selectAndApplyModel is gated behind verbose flag", () => {
	// The Model [phase] [tier] notification should only fire when verbose=true.
	// The dashboard header already shows the active model, so the notification
	// is redundant noise during auto-mode (#3719).
	const sfDir = join(__dirname, "..");
	const src = readFileSync(join(sfDir, "auto-model-selection.ts"), "utf-8");

	// Find the block where setModel succeeds (appliedModel = model) and
	// verify notify is inside an `if (verbose)` guard.
	const setModelBlock = src.match(
		/const ok = await pi\.setModel\(model[\s\S]*?appliedModel = model;([\s\S]*?)break;/,
	);
	assert.ok(setModelBlock, "should find the setModel success block");

	const blockBody = setModelBlock![1];
	// The notify call must be inside an if (verbose) block
	assert.ok(
		blockBody.includes("if (verbose)"),
		"Model change ctx.ui.notify must be gated behind if (verbose) to avoid auto-mode notification noise",
	);
	assert.ok(
		blockBody.includes("ctx.ui.notify"),
		"notify call should still exist (just verbose-gated)",
	);
});

test("model policy resolves candidates from the policy-eligible pool", () => {
	const src = readFileSync(
		join(__dirname, "..", "auto-model-selection.ts"),
		"utf-8",
	);
	assert.ok(
		/const\s+resolutionPool\s*=\s*uokFlags\.modelPolicy\s*\?\s*routingEligibleModels\s*:\s*availableModels/.test(
			src,
		),
		"selectAndApplyModel should resolve model IDs against policy-eligible models when model policy is enabled",
	);
});

test("model policy receives task metadata for requirement-vector decisions", () => {
	const src = readFileSync(
		join(__dirname, "..", "auto-model-selection.ts"),
		"utf-8",
	);
	assert.ok(
		src.includes("taskMetadata: taskMetadataForPolicy"),
		"applyModelPolicyFilter should receive task metadata so requirement vectors are unit-aware",
	);
	assert.ok(
		src.includes("extractTaskMetadata(unitId, basePath)"),
		"execute-task dispatch should derive metadata before policy filtering",
	);
});

test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
	const availableModels = [
		{ id: "claude-sonnet-4-6", provider: "claude-code" },
		{ id: "claude-sonnet-4-6", provider: "anthropic" },
	];

	// When the session is NOT on claude-code, bare IDs should resolve to
	// the canonical anthropic provider (original #2905 behavior preserved).
	const result = resolveModelId(
		"claude-sonnet-4-6",
		availableModels,
		undefined,
	);
	assert.ok(result, "should resolve a model");
	assert.equal(
		result.provider,
		"anthropic",
		"anthropic must win when session is not claude-code",
	);
});

test("resolveModelId: claude-code wins when session is claude-code regardless of list order", () => {
	const availableModels = [
		{ id: "claude-sonnet-4-6", provider: "claude-code" },
		{ id: "claude-sonnet-4-6", provider: "anthropic" },
	];

	// When session provider is claude-code (subscription user migration), it must
	// win regardless of candidate ordering to avoid the third-party block (#3772).
	const result = resolveModelId(
		"claude-sonnet-4-6",
		availableModels,
		"claude-code",
	);
	assert.ok(result, "should resolve a model");
	assert.equal(
		result.provider,
		"claude-code",
		"claude-code must win when it is the session provider",
	);
});

// ─── advisor_allowed_providers tests ─────────────────────────────────────

test("selectAndApplyModel enforces advisor_allowed_providers for subagent units", async () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = makeTempDir("sf-advisor-providers-project-");
	const tempSfHome = makeTempDir("sf-advisor-providers-home-");

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });
		writeFileSync(
			join(tempSfHome, "preferences.md"),
			[
				"---",
				"version: 1",
				"advisor_allowed_providers:",
				"  - anthropic",
				"models:",
				"  subagent:",
				"    model: openai/gpt-4o",
				"    fallbacks:",
				"      - anthropic/claude-sonnet-4-6",
				"---",
			].join("\n"),
			"utf-8",
		);
		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			["---", "version: 1", "models: {}", "---"].join("\n"),
			"utf-8",
		);
		process.env.SF_HOME = tempSfHome;
		process.chdir(tempProject);

		const availableModels = [
			{ id: "gpt-4o", provider: "openai", api: "openai-completions" },
			{ id: "claude-sonnet-4-6", provider: "anthropic", api: "anthropic-messages" },
		] as any[];
		const selectedModels: any[] = [];
		const notifications: Array<{ message: string; type: string }> = [];

		const ctx = {
			sessionManager: { getSessionId: () => "test-session" },
			modelRegistry: { getAvailable: () => availableModels },
			model: { id: "claude-sonnet-4-6", provider: "anthropic" },
			ui: {
				notify: (message: string, type: string) => {
					notifications.push({ message, type });
				},
			},
		} as any;
		const pi = {
			getActiveTools: () => [],
			setActiveTools: () => undefined,
			setModel: async (model: any) => {
				selectedModels.push(model);
				return true;
			},
			emitBeforeModelSelect: async () => undefined,
			emitAdjustToolSet: async () => undefined,
		} as any;

		const result = await selectAndApplyModel(
			ctx,
			pi,
			"subagent/research",
			"M999/S01/T01",
			tempProject,
			{
				advisor_allowed_providers: ["anthropic"],
			},
			true,
			null,
		);

		// Should skip openai/gpt-4o (not in advisor_allowed_providers) and
		// fall back to anthropic/claude-sonnet-4-6 (the session model, which is allowed).
		assert.equal(
			result.appliedModel?.provider,
			"anthropic",
			"subagent must use a provider in advisor_allowed_providers",
		);
		assert.equal(result.appliedModel?.id, "claude-sonnet-4-6");
		assert.ok(
			notifications.some((n) =>
				n.message.includes("advisor_allowed_providers"),
			),
			"should warn when skipping a provider due to advisor_allowed_providers",
		);
	} 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(tempSfHome, { recursive: true, force: true });
	}
});

test("selectAndApplyModel does not enforce advisor_allowed_providers for non-subagent units", async () => {
	const originalCwd = process.cwd();
	const originalSfHome = process.env.SF_HOME;
	const tempProject = makeTempDir("sf-advisor-providers-project-");
	const tempSfHome = makeTempDir("sf-advisor-providers-home-");

	try {
		mkdirSync(join(tempProject, ".sf"), { recursive: true });
		writeFileSync(
			join(tempSfHome, "preferences.md"),
			[
				"---",
				"version: 1",
				"advisor_allowed_providers:",
				"  - anthropic",
				"models:",
				"  execution: openai/gpt-4o",
				"---",
			].join("\n"),
			"utf-8",
		);
		writeFileSync(
			join(tempProject, ".sf", "PREFERENCES.md"),
			["---", "version: 1", "models: {}", "---"].join("\n"),
			"utf-8",
		);
		process.env.SF_HOME = tempSfHome;
		process.chdir(tempProject);

		const availableModels = [
			{ id: "gpt-4o", provider: "openai", api: "openai-completions" },
			{ id: "claude-sonnet-4-6", provider: "anthropic", api: "anthropic-messages" },
		] as any[];
		const selectedModels: any[] = [];

		const ctx = {
			sessionManager: { getSessionId: () => "test-session" },
			modelRegistry: { getAvailable: () => availableModels },
			model: { id: "claude-sonnet-4-6", provider: "anthropic" },
			ui: { notify: () => undefined },
		} as any;
		const pi = {
			getActiveTools: () => [],
			setActiveTools: () => undefined,
			setModel: async (model: any) => {
				selectedModels.push(model);
				return true;
			},
			emitBeforeModelSelect: async () => undefined,
			emitAdjustToolSet: async () => undefined,
		} as any;

		const result = await selectAndApplyModel(
			ctx,
			pi,
			"execute-task",
			"M999/S01/T01",
			tempProject,
			undefined,
			true,
			null,
		);

		// Execution units should not be gated by advisor_allowed_providers.
		assert.equal(
			result.appliedModel?.provider,
			"openai",
			"non-subagent unit should not be affected by advisor_allowed_providers",
		);
		assert.equal(result.appliedModel?.id, "gpt-4o");
	} 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(tempSfHome, { recursive: true, force: true });
	}
});

test("advisor_allowed_providers guard is present in selectAndApplyModel source", () => {
	const sfDir = join(__dirname, "..");
	const src = readFileSync(
		join(sfDir, "auto-model-selection.ts"),
		"utf-8",
	);

	assert.ok(
		src.includes("isProviderAllowedForAdvisor(model.provider, prefs)"),
		"selectAndApplyModel must call isProviderAllowedForAdvisor when selecting models for subagent units",
	);
	assert.ok(
		/advisor_allowed_providers/.test(src),
		"auto-model-selection.ts must reference advisor_allowed_providers",
	);
	assert.ok(
		/unitType === "subagent" \|\| unitType\.startsWith\("subagent\/"\)/.test(src),
		"advisor_allowed_providers gate must only apply to subagent unit types",
	);
});
