import { buildSettingsPatch } from "@/lib/settingsDiff";
import type { Settings } from "@/lib/types";

function baseSettings(): Settings {
	return {
		upstream: { attempt_timeout_ms: 2500, read_timeout_ms: 3000, total_timeout_ms: 5000 },
		dns: { bind_ipv4: "0.0.0.0", bind_ipv6: "::", port: 53, rate_limit: 100, rate_window_seconds: 60 },
		blocking: { response: "zero", ttl: 300 },
		cache: { size: 10000, negative_ttl_max: 300 },
		web: {
			enabled: true,
			bind: "127.0.0.1",
			port: 8080,
			session_ttl_hours: 24,
			api_rate_limit_per_min: 60,
			api_localhost_exempt: true,
			sse_max_connections_per_ip: 2,
			trusted_proxies: "",
			auth_enabled: true,
		},
		doh_server: { enabled: false, bind: "0.0.0.0", port: 443, cert_path: "", key_path: "" },
		dot_server: { enabled: false, bind: "0.0.0.0", port: 853, cert_path: "", key_path: "" },
		edns: { ecs_mode: "strip" },
		logging: {
			level: "info",
			retention_days: 30,
			query_log_buffer_max: 10000,
			query_log_flush_interval_s: 60,
			hide_domains: false,
			hide_client_ips: false,
			output: "stderr",
			file_path: "",
			max_size_mb: 50,
			max_files: 3,
		},
		disk: { min_free_mb: 100, warn_free_mb: 500 },
		blocklist_update: { enabled: true, interval_hours: 24 },
	};
}

function edit(mutate: (s: Settings) => void): Settings {
	const edited = structuredClone(baseSettings());
	mutate(edited);
	return edited;
}

test("no changes and no password produces null", () => {
	expect(buildSettingsPatch(baseSettings(), baseSettings())).toBeNull();
});

test("an empty password is not a change", () => {
	expect(buildSettingsPatch(baseSettings(), baseSettings(), "")).toBeNull();
});

test("a single scalar change patches only its section field", () => {
	const edited = edit((s) => {
		s.dns.port = 5353;
	});
	expect(buildSettingsPatch(baseSettings(), edited)).toEqual({ dns: { port: 5353 } });
});

test("changes across sections stay grouped and minimal", () => {
	const edited = edit((s) => {
		s.logging.level = "debug";
		s.logging.retention_days = 7;
		s.cache.size = 20000;
	});
	expect(buildSettingsPatch(baseSettings(), edited)).toEqual({
		cache: { size: 20000 },
		logging: { level: "debug", retention_days: 7 },
	});
});

test("tls listener sections diff like any other", () => {
	const edited = edit((s) => {
		s.dot_server.enabled = true;
		s.dot_server.cert_path = "/etc/nxdns/dot.pem";
	});
	expect(buildSettingsPatch(baseSettings(), edited)).toEqual({
		dot_server: { enabled: true, cert_path: "/etc/nxdns/dot.pem" },
	});
});

test("web.auth_enabled is never emitted even when it differs", () => {
	const edited = edit((s) => {
		s.web.auth_enabled = false;
		s.web.port = 9090;
	});
	expect(buildSettingsPatch(baseSettings(), edited)).toEqual({ web: { port: 9090 } });
});

test("a password alone produces a web-only patch", () => {
	expect(buildSettingsPatch(baseSettings(), baseSettings(), "hunter2")).toEqual({
		web: { password: "hunter2" },
	});
});

test("a password merges into an existing web section diff", () => {
	const edited = edit((s) => {
		s.web.session_ttl_hours = 48;
	});
	expect(buildSettingsPatch(baseSettings(), edited, "hunter2")).toEqual({
		web: { session_ttl_hours: 48, password: "hunter2" },
	});
});
