/**
 * Offline mode support tests.
 *
 * Covers:
 * - isLocalModel() detection for local vs cloud URLs
 * - isAllLocalChain() aggregate check
 * - Auto-detection sets PI_OFFLINE when all models are local
 * - Validation rejects remote models with --offline flag
 * - Network error codes in INFRA_ERROR_CODES
 * - Web search tool filtered when PI_OFFLINE is set
 *
 * Fixes #2341
 */

import assert from "node:assert/strict";
import { test } from "vitest";
import { isLocalModel } from "../../packages/coding-agent/src/core/local-model-check.ts";

// ─── isLocalModel ───────────────────────────────────────────────────────────

test("isLocalModel returns true for localhost", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "http://localhost:11434" })),
		true,
	);
});

test("isLocalModel returns true for 127.0.0.1", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "http://127.0.0.1:8080/v1" })),
		true,
	);
});

test("isLocalModel returns true for 0.0.0.0", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "http://0.0.0.0:1234" })),
		true,
	);
});

test("isLocalModel returns true for ::1 (IPv6 loopback)", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "http://[::1]:11434" })),
		true,
	);
});

test("isLocalModel returns true for unix socket path", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "unix:///var/run/ollama.sock" })),
		true,
	);
});

test("isLocalModel returns false for api.anthropic.com", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "https://api.anthropic.com" })),
		false,
	);
});

test("isLocalModel returns false for api.openai.com", () => {
	assert.strictEqual(
		isLocalModel(fakeModel({ baseUrl: "https://api.openai.com/v1" })),
		false,
	);
});

test("isLocalModel returns false when no baseUrl (empty string = cloud)", () => {
	assert.strictEqual(isLocalModel(fakeModel({ baseUrl: "" })), false);
});

// ─── isAllLocalChain (source-level check) ───────────────────────────────────

test("isAllLocalChain returns true when all models are local (logic check)", () => {
	const models = [
		fakeModel({ baseUrl: "http://localhost:11434/v1" }),
		fakeModel({ baseUrl: "http://127.0.0.1:8080" }),
	];
	assert.strictEqual(
		models.every((m) => isLocalModel(m)),
		true,
	);
});

test("isAllLocalChain returns false when mixed local and remote", () => {
	const models = [
		fakeModel({ baseUrl: "http://localhost:11434/v1" }),
		fakeModel({ baseUrl: "https://api.anthropic.com" }),
	];
	assert.strictEqual(
		models.every((m) => isLocalModel(m)),
		false,
	);
});

test("isAllLocalChain returns false for empty list", () => {
	const models: Array<{ baseUrl: string }> = [];
	// Empty => false (no models means we can't guarantee local)
	assert.strictEqual(
		models.length === 0 ? false : models.every((m) => isLocalModel(m)),
		false,
	);
});

// ─── INFRA_ERROR_CODES includes network errors ─────────────────────────────

test("INFRA_ERROR_CODES includes ECONNREFUSED", async () => {
	const { INFRA_ERROR_CODES } = await import(
		"../../src/resources/extensions/sf/auto/infra-errors.ts"
	);
	assert.strictEqual(INFRA_ERROR_CODES.has("ECONNREFUSED"), true);
});

test("INFRA_ERROR_CODES includes ENOTFOUND", async () => {
	const { INFRA_ERROR_CODES } = await import(
		"../../src/resources/extensions/sf/auto/infra-errors.ts"
	);
	assert.strictEqual(INFRA_ERROR_CODES.has("ENOTFOUND"), true);
});

test("INFRA_ERROR_CODES includes ENETUNREACH", async () => {
	const { INFRA_ERROR_CODES } = await import(
		"../../src/resources/extensions/sf/auto/infra-errors.ts"
	);
	assert.strictEqual(INFRA_ERROR_CODES.has("ENETUNREACH"), true);
});

// ─── isInfrastructureError detects network errors in offline mode ───────────

test("isInfrastructureError returns code for ECONNREFUSED when offline", async () => {
	const { isInfrastructureError } = await import(
		"../../src/resources/extensions/sf/auto/infra-errors.ts"
	);
	const savedOffline = process.env.PI_OFFLINE;
	process.env.PI_OFFLINE = "1";
	try {
		const err = Object.assign(new Error("connect ECONNREFUSED"), {
			code: "ECONNREFUSED",
		});
		assert.strictEqual(isInfrastructureError(err), "ECONNREFUSED");
	} finally {
		if (savedOffline === undefined) delete process.env.PI_OFFLINE;
		else process.env.PI_OFFLINE = savedOffline;
	}
});

// ─── Web search filtering when PI_OFFLINE set ──────────────────────────────

test("web search tool is filtered when PI_OFFLINE is set", async () => {
	const { readFileSync } = await import("node:fs");
	const { join } = await import("node:path");

	const toolExecPath = join(
		process.cwd(),
		"packages/coding-agent/src/modes/interactive/components/tool-execution.ts",
	);
	const content = readFileSync(toolExecPath, "utf-8");
	assert.ok(
		content.includes("PI_OFFLINE") && content.includes("web_search"),
		"tool-execution.ts should check PI_OFFLINE for web_search",
	);

	const chatControllerPath = join(
		process.cwd(),
		"packages/coding-agent/src/modes/interactive/controllers/chat-controller.ts",
	);
	const chatContent = readFileSync(chatControllerPath, "utf-8");
	assert.ok(
		chatContent.includes("PI_OFFLINE") &&
			chatContent.includes("webSearchResult"),
		"chat-controller.ts should check PI_OFFLINE for webSearchResult",
	);
});

// ─── Version check skipped when PI_OFFLINE ─────────────────────────────────

test("version check is skipped when PI_OFFLINE is set", async () => {
	const { readFileSync } = await import("node:fs");
	const { join } = await import("node:path");

	const interactivePath = join(
		process.cwd(),
		"packages/coding-agent/src/modes/interactive/interactive-mode.ts",
	);
	const content = readFileSync(interactivePath, "utf-8");
	assert.ok(
		content.includes("PI_OFFLINE"),
		"interactive-mode.ts should check PI_OFFLINE for version check skip",
	);
});

// ─── Helper ─────────────────────────────────────────────────────────────────

function fakeModel(overrides: Partial<{ baseUrl: string }> = {}): {
	baseUrl: string;
} {
	return { baseUrl: overrides.baseUrl ?? "" };
}
