/**
 * Unit tests for init internals — decision tree primitives + plugin sync.
 *
 * Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
 * in sync — if you add a behavioral assertion here, mirror it on the Python
 * side and vice versa.
 *
 *  - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
 *    flap silently mints a new shadow over a working key).
 *  - `plugin_sync` must only update entries that already exist, preserve
 *    trailing newlines, and never mangle other lines.
 */

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { pingKey } from "../src/commands/init.js";
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";

// ── pingKey ──────────────────────────────────────────────────────────────

describe("pingKey — network vs auth distinction", () => {
	const origFetch = globalThis.fetch;
	afterEach(() => {
		globalThis.fetch = origFetch;
		vi.restoreAllMocks();
	});

	it("returns true for 200", async () => {
		globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
		await expect(pingKey("k", "http://x")).resolves.toBe(true);
	});

	it("returns false for 401 (definitively invalid)", async () => {
		globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
		await expect(pingKey("k", "http://x")).resolves.toBe(false);
	});

	it("returns false for 403 (definitively invalid)", async () => {
		globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
		await expect(pingKey("k", "http://x")).resolves.toBe(false);
	});

	it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
		globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
		await expect(pingKey("k", "http://x")).resolves.toBe(true);
	});

	it("returns true on network error (prefer reuse over re-mint)", async () => {
		globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
		await expect(pingKey("k", "http://x")).resolves.toBe(true);
	});

	it("returns true on timeout (prefer reuse)", async () => {
		globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
		await expect(pingKey("k", "http://x")).resolves.toBe(true);
	});
});

// ── updateShellRc ────────────────────────────────────────────────────────

describe("updateShellRc — exists-only contract", () => {
	let tmpDir: string;

	beforeEach(() => {
		tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dejavu-test-"));
	});
	afterEach(() => {
		fs.rmSync(tmpDir, { recursive: true, force: true });
	});

	it("updates existing export and preserves trailing newline", () => {
		const rc = path.join(tmpDir, ".zshrc");
		fs.writeFileSync(rc, 'export VENICE_API_KEY="old"\n');
		expect(updateShellRc(rc, "newkey")).toBe(true);
		expect(fs.readFileSync(rc, "utf-8")).toBe('export VENICE_API_KEY="newkey"\n');
	});

	it("does NOT create a new export when none exists", () => {
		const rc = path.join(tmpDir, ".zshrc");
		fs.writeFileSync(rc, "alias ll='ls -la'\n");
		expect(updateShellRc(rc, "newkey")).toBe(false);
		expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
	});

	it("preserves surrounding content", () => {
		const rc = path.join(tmpDir, ".zshrc");
		const original =
			"# my zshrc\n" +
			"alias ll='ls -la'\n" +
			"export VENICE_API_KEY='old'\n" +
			"export OTHER=keepme\n";
		fs.writeFileSync(rc, original);
		updateShellRc(rc, "newkey");
		const after = fs.readFileSync(rc, "utf-8");
		expect(after).toContain("alias ll='ls -la'\n");
		expect(after).toContain("export OTHER=keepme\n");
		expect(after).toContain("# my zshrc\n");
		expect(after).toContain('export VENICE_API_KEY="newkey"\n');
	});

	it("is idempotent when value already matches", () => {
		const rc = path.join(tmpDir, ".zshrc");
		fs.writeFileSync(rc, 'export VENICE_API_KEY="same"\n');
		expect(updateShellRc(rc, "same")).toBe(false);
	});

	it("is a no-op for missing files", () => {
		const rc = path.join(tmpDir, ".zshrc"); // does not exist
		expect(updateShellRc(rc, "x")).toBe(false);
	});
});

// ── updateClaudeSettings ─────────────────────────────────────────────────

describe("updateClaudeSettings — never creates entries", () => {
	let tmpDir: string;

	beforeEach(() => {
		tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dejavu-test-"));
	});
	afterEach(() => {
		fs.rmSync(tmpDir, { recursive: true, force: true });
	});

	it("does not create env block when none exists", () => {
		const settings = path.join(tmpDir, "settings.json");
		fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
		expect(updateClaudeSettings(settings, "newkey")).toBe(false);
		expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
			otherKey: 1,
		});
	});

	it("does not create VENICE_API_KEY entry in existing env block", () => {
		const settings = path.join(tmpDir, "settings.json");
		fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
		expect(updateClaudeSettings(settings, "newkey")).toBe(false);
	});

	it("updates existing entry and preserves siblings", () => {
		const settings = path.join(tmpDir, "settings.json");
		fs.writeFileSync(
			settings,
			JSON.stringify({ env: { VENICE_API_KEY: "old", OTHER: "y" } }, null, 2),
		);
		expect(updateClaudeSettings(settings, "fresh")).toBe(true);
		const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
		expect(data.env.VENICE_API_KEY).toBe("fresh");
		expect(data.env.OTHER).toBe("y");
	});

	it("is idempotent when value already matches", () => {
		const settings = path.join(tmpDir, "settings.json");
		fs.writeFileSync(
			settings,
			JSON.stringify({ env: { VENICE_API_KEY: "same" } }),
		);
		expect(updateClaudeSettings(settings, "same")).toBe(false);
	});

	it("is a no-op for malformed JSON", () => {
		const settings = path.join(tmpDir, "settings.json");
		fs.writeFileSync(settings, "{ this is not json");
		expect(updateClaudeSettings(settings, "x")).toBe(false);
	});
});
