import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";

import {
	checkForUpdates,
	compareSemver,
	fetchLatestVersionFromRegistry,
	readUpdateCache,
	writeUpdateCache,
} from "../update-check.js";

// ---------------------------------------------------------------------------
// compareSemver
// ---------------------------------------------------------------------------

test("compareSemver returns 0 for equal versions", () => {
	assert.equal(compareSemver("1.0.0", "1.0.0"), 0);
	assert.equal(compareSemver("2.8.3", "2.8.3"), 0);
});

test("compareSemver returns 1 when first is greater", () => {
	assert.equal(compareSemver("2.0.0", "1.0.0"), 1);
	assert.equal(compareSemver("1.1.0", "1.0.0"), 1);
	assert.equal(compareSemver("1.0.1", "1.0.0"), 1);
	assert.equal(compareSemver("2.8.3", "2.7.1"), 1);
});

test("compareSemver returns -1 when first is smaller", () => {
	assert.equal(compareSemver("1.0.0", "2.0.0"), -1);
	assert.equal(compareSemver("1.0.0", "1.1.0"), -1);
	assert.equal(compareSemver("1.0.0", "1.0.1"), -1);
	assert.equal(compareSemver("2.3.11", "2.8.3"), -1);
});

test("compareSemver handles versions with different segment counts", () => {
	assert.equal(compareSemver("1.0", "1.0.0"), 0);
	assert.equal(compareSemver("1.0.0", "1.0"), 0);
	assert.equal(compareSemver("1.0", "1.0.1"), -1);
	assert.equal(compareSemver("1.0.1", "1.0"), 1);
});

// ---------------------------------------------------------------------------
// readUpdateCache / writeUpdateCache
// ---------------------------------------------------------------------------

test("readUpdateCache returns null for nonexistent file", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-cache-"));
	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const result = readUpdateCache(join(tmp, "nonexistent"));
	assert.equal(result, null);
});

test("readUpdateCache returns null for malformed JSON", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-cache-"));
	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const cachePath = join(tmp, ".update-check");
	writeFileSync(cachePath, "not json");
	const result = readUpdateCache(cachePath);
	assert.equal(result, null);
});

test("writeUpdateCache + readUpdateCache round-trips correctly", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-cache-"));
	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const cachePath = join(tmp, ".update-check");
	const cache = { lastCheck: Date.now(), latestVersion: "3.0.0" };
	writeUpdateCache(cache, cachePath);
	const result = readUpdateCache(cachePath);
	assert.deepEqual(result, cache);
});

test("writeUpdateCache creates parent directories", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-cache-"));
	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const cachePath = join(tmp, "nested", "dir", ".update-check");
	writeUpdateCache(
		{ lastCheck: Date.now(), latestVersion: "1.0.0" },
		cachePath,
	);
	const raw = readFileSync(cachePath, "utf-8");
	assert.ok(raw.includes("1.0.0"));
});

// ---------------------------------------------------------------------------
// checkForUpdates — integration tests with a local HTTP server
// ---------------------------------------------------------------------------

function startMockRegistry(
	responseBody: object,
	statusCode = 200,
): Promise<{ url: string; close: () => Promise<void> }> {
	return new Promise((resolve) => {
		const server = createServer((_req, res) => {
			res.writeHead(statusCode, { "Content-Type": "application/json" });
			res.end(JSON.stringify(responseBody));
		});
		server.listen(0, "127.0.0.1", () => {
			const addr = server.address() as { port: number };
			resolve({
				url: `http://127.0.0.1:${addr.port}`,
				close: () => new Promise<void>((r) => server.close(() => r())),
			});
		});
	});
}

test("checkForUpdates calls onUpdate when newer version is available", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const registry = await startMockRegistry({ version: "99.0.0" });
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;
	let reportedCurrent = "";
	let reportedLatest = "";

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath: join(tmp, ".update-check"),
		registryUrl: registry.url,
		checkIntervalMs: 0,
		fetchTimeoutMs: 5000,
		onUpdate: (current, latest) => {
			called = true;
			reportedCurrent = current;
			reportedLatest = latest;
		},
	});

	assert.ok(called, "onUpdate should have been called");
	assert.equal(reportedCurrent, "1.0.0");
	assert.equal(reportedLatest, "99.0.0");
});

test("checkForUpdates does not call onUpdate when already on latest", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const registry = await startMockRegistry({ version: "1.0.0" });
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath: join(tmp, ".update-check"),
		registryUrl: registry.url,
		checkIntervalMs: 0,
		fetchTimeoutMs: 5000,
		onUpdate: () => {
			called = true;
		},
	});

	assert.ok(!called, "onUpdate should not be called when versions match");
});

test("checkForUpdates does not call onUpdate when current is ahead", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const registry = await startMockRegistry({ version: "1.0.0" });
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;

	await checkForUpdates({
		currentVersion: "2.0.0",
		cachePath: join(tmp, ".update-check"),
		registryUrl: registry.url,
		checkIntervalMs: 0,
		fetchTimeoutMs: 5000,
		onUpdate: () => {
			called = true;
		},
	});

	assert.ok(!called, "onUpdate should not be called when current is ahead");
});

test("checkForUpdates writes cache after successful fetch", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const cachePath = join(tmp, ".update-check");
	const registry = await startMockRegistry({ version: "5.0.0" });
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath,
		registryUrl: registry.url,
		checkIntervalMs: 0,
		fetchTimeoutMs: 5000,
		onUpdate: () => {},
	});

	const cache = readUpdateCache(cachePath);
	assert.ok(cache, "cache should exist after fetch");
	assert.equal(cache!.latestVersion, "5.0.0");
	assert.ok(cache!.lastCheck > 0);
});

test("checkForUpdates uses cache and skips fetch when checked recently", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const cachePath = join(tmp, ".update-check");
	// Write a fresh cache entry
	writeUpdateCache(
		{ lastCheck: Date.now(), latestVersion: "10.0.0" },
		cachePath,
	);

	// Start server that would return a different version — should NOT be reached
	const registry = await startMockRegistry({ version: "20.0.0" });
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	let reportedLatest = "";

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath,
		registryUrl: registry.url,
		checkIntervalMs: 60 * 60 * 1000, // 1 hour
		fetchTimeoutMs: 5000,
		onUpdate: (_current, latest) => {
			reportedLatest = latest;
		},
	});

	// Should use cached version (10.0.0), not the server's (20.0.0)
	assert.equal(reportedLatest, "10.0.0");
});

test("checkForUpdates skips notification when cache is fresh and versions match", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const cachePath = join(tmp, ".update-check");
	writeUpdateCache(
		{ lastCheck: Date.now(), latestVersion: "1.0.0" },
		cachePath,
	);

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath,
		checkIntervalMs: 60 * 60 * 1000,
		fetchTimeoutMs: 5000,
		onUpdate: () => {
			called = true;
		},
	});

	assert.ok(
		!called,
		"onUpdate should not be called when cached version matches current",
	);
});

test("checkForUpdates handles server error gracefully", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const registry = await startMockRegistry({}, 500);
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath: join(tmp, ".update-check"),
		registryUrl: registry.url,
		checkIntervalMs: 0,
		fetchTimeoutMs: 5000,
		onUpdate: () => {
			called = true;
		},
	});

	assert.ok(!called, "onUpdate should not be called on server error");
});

test("checkForUpdates handles network timeout gracefully", async (_t) => {
	// Start a server that never responds
	const server = createServer(() => {
		/* intentionally never respond */
	});
	await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
	const addr = server.address() as { port: number };
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));

	afterEach(async () => {
		await new Promise<void>((r) => server.close(() => r()));
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath: join(tmp, ".update-check"),
		registryUrl: `http://127.0.0.1:${addr.port}`,
		checkIntervalMs: 0,
		fetchTimeoutMs: 500, // Very short timeout
		onUpdate: () => {
			called = true;
		},
	});

	assert.ok(!called, "onUpdate should not be called on timeout");
});

test("checkForUpdates handles missing version field in response", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-update-"));
	const registry = await startMockRegistry({ name: "singularity-forge" }); // no version field
	afterEach(async () => {
		await registry.close();
		rmSync(tmp, { recursive: true, force: true });
	});

	let called = false;

	await checkForUpdates({
		currentVersion: "1.0.0",
		cachePath: join(tmp, ".update-check"),
		registryUrl: registry.url,
		checkIntervalMs: 0,
		fetchTimeoutMs: 5000,
		onUpdate: () => {
			called = true;
		},
	});

	assert.ok(
		!called,
		"onUpdate should not be called when response has no version",
	);
});

test("fetchLatestVersionFromRegistry returns the registry version string", async (_t) => {
	const registry = await startMockRegistry({ version: "2.67.0" });
	afterEach(async () => {
		await registry.close();
	});

	const latest = await fetchLatestVersionFromRegistry(registry.url, 5000);
	assert.equal(latest, "2.67.0");
});

test("fetchLatestVersionFromRegistry returns null for blank version strings", async (_t) => {
	const registry = await startMockRegistry({ version: "" });
	afterEach(async () => {
		await registry.close();
	});

	const latest = await fetchLatestVersionFromRegistry(registry.url, 5000);
	assert.equal(latest, null);
});
