import type { SurfaceSpec } from "@dispatch/ui-contract";
import { describe, expect, it } from "vitest";
import {
	clampMinutes,
	clampSeconds,
	colorClass,
	formatCountdown,
	formatWarmLabel,
	fromMinSec,
	initialWarmingState,
	observeWarm,
	parseControls,
	parsePct,
	secondsUntilNext,
	statusForPct,
	toMinSec,
} from "./view-model";

const spec = (fields: SurfaceSpec["fields"]): SurfaceSpec => ({
	id: "cache-warming",
	region: "side",
	title: "Cache Warming",
	fields,
});

describe("parsePct", () => {
	it("parses a percentage string", () => {
		expect(parsePct("100%")).toBe(100);
		expect(parsePct("93 %")).toBe(93);
		expect(parsePct("0%")).toBe(0);
	});
	it("returns null for a dash / non-numeric", () => {
		expect(parsePct("—")).toBeNull();
		expect(parsePct("n/a")).toBeNull();
	});
});

describe("parseControls", () => {
	it("returns empty defaults for a null spec", () => {
		const c = parseControls(null);
		expect(c).toEqual({
			enabled: false,
			toggleActionId: null,
			intervalSeconds: 0,
			setIntervalActionId: null,
			lastPct: null,
			retentionPct: null,
			nextWarmAt: null,
			lastWarmAt: null,
		});
	});

	it("extracts toggle / number / both stats / timer by kind", () => {
		const c = parseControls(
			spec([
				{
					kind: "toggle",
					label: "Enabled",
					value: true,
					action: { actionId: "cache-warming/toggle" },
				},
				{
					kind: "number",
					label: "Interval",
					value: 240,
					unit: "s",
					action: { actionId: "cache-warming/set-interval" },
				},
				{ kind: "stat", label: "Last cache rate", value: "61%" },
				{ kind: "stat", label: "Cache retention", value: "100%" },
				{
					kind: "custom",
					rendererId: "cache-warming-timer",
					payload: { nextWarmAt: 1_700_000_240_000, lastWarmAt: 1_700_000_000_000 },
				},
			]),
		);
		expect(c).toEqual({
			enabled: true,
			toggleActionId: "cache-warming/toggle",
			intervalSeconds: 240,
			setIntervalActionId: "cache-warming/set-interval",
			lastPct: 61,
			retentionPct: 100,
			nextWarmAt: 1_700_000_240_000,
			lastWarmAt: 1_700_000_000_000,
		});
	});

	it("tells the retention stat apart from the rate stat by label", () => {
		const c = parseControls(
			spec([
				{ kind: "stat", label: "Cache retention", value: "100%" },
				{ kind: "stat", label: "Last cache rate", value: "61%" },
			]),
		);
		expect(c.retentionPct).toBe(100);
		expect(c.lastPct).toBe(61);
	});

	it("treats a '—' stat as no pct", () => {
		const c = parseControls(spec([{ kind: "stat", label: "Last cache rate", value: "—" }]));
		expect(c.lastPct).toBeNull();
	});

	it("ignores an unknown custom renderer and a malformed timer payload", () => {
		const c = parseControls(
			spec([
				{ kind: "custom", rendererId: "something-else", payload: { nextWarmAt: 5 } },
				{ kind: "custom", rendererId: "cache-warming-timer", payload: "nope" },
			]),
		);
		expect(c.nextWarmAt).toBeNull();
		expect(c.lastWarmAt).toBeNull();
	});
});

describe("interval ↔ min/sec", () => {
	it("clampSeconds caps at 0..59", () => {
		expect(clampSeconds(75)).toBe(59);
		expect(clampSeconds(-3)).toBe(0);
		expect(clampSeconds(30)).toBe(30);
		expect(clampSeconds(Number.NaN)).toBe(0);
	});
	it("clampMinutes floors at 0", () => {
		expect(clampMinutes(-1)).toBe(0);
		expect(clampMinutes(4)).toBe(4);
	});
	it("toMinSec splits total seconds", () => {
		expect(toMinSec(240)).toEqual({ minutes: 4, seconds: 0 });
		expect(toMinSec(125)).toEqual({ minutes: 2, seconds: 5 });
		expect(toMinSec(45)).toEqual({ minutes: 0, seconds: 45 });
	});
	it("fromMinSec combines (clamping seconds to 59)", () => {
		expect(fromMinSec(4, 0)).toBe(240);
		expect(fromMinSec(2, 5)).toBe(125);
		expect(fromMinSec(1, 75)).toBe(119); // 75s clamped to 59
	});
});

describe("status + formatting", () => {
	it("statusForPct buckets high/mid/low", () => {
		expect(statusForPct(100)).toBe("success");
		expect(statusForPct(80)).toBe("success");
		expect(statusForPct(60)).toBe("warning");
		expect(statusForPct(40)).toBe("warning");
		expect(statusForPct(10)).toBe("error");
	});
	it("colorClass maps to literal DaisyUI classes", () => {
		expect(colorClass("success")).toBe("text-success");
		expect(colorClass("warning")).toBe("text-warning");
		expect(colorClass("error")).toBe("text-error");
	});
	it("formatWarmLabel matches the manual-warm phrasing", () => {
		expect(formatWarmLabel(100)).toBe("Warmed — 100% cache hit");
		expect(formatWarmLabel(92.6)).toBe("Warmed — 93% cache hit");
	});
	it("formatCountdown renders s and m:ss", () => {
		expect(formatCountdown(9)).toBe("9s");
		expect(formatCountdown(59)).toBe("59s");
		expect(formatCountdown(60)).toBe("1:00");
		expect(formatCountdown(185)).toBe("3:05");
		expect(formatCountdown(-5)).toBe("0s");
	});
});

describe("warming history reducer (observeWarm)", () => {
	it("starts empty", () => {
		const s = initialWarmingState();
		expect(s.history).toEqual([]);
		expect(s.lastWarmAt).toBeNull();
	});

	it("records a new entry on each new authoritative lastWarmAt", () => {
		let s = initialWarmingState();
		s = observeWarm(s, 1000, 100);
		s = observeWarm(s, 2000, 90);
		expect(s.history).toEqual([
			{ pct: 90, at: 2000 },
			{ pct: 100, at: 1000 },
		]);
		expect(s.lastWarmAt).toBe(2000);
	});

	it("de-duplicates on the timestamp, not the pct (a re-pushed surface → no dup)", () => {
		let s = initialWarmingState();
		s = observeWarm(s, 1000, 100); // warm
		s = observeWarm(s, 1000, 100); // toggle/interval re-push, same lastWarmAt → skip
		expect(s.history).toHaveLength(1);
	});

	it("records two warms with the SAME pct (distinct timestamps both count)", () => {
		let s = initialWarmingState();
		s = observeWarm(s, 1000, 100);
		s = observeWarm(s, 2000, 100);
		expect(s.history.map((e) => e.at)).toEqual([2000, 1000]);
	});

	it("ignores a null lastWarmAt; a null pct advances the key without an entry", () => {
		let s = initialWarmingState();
		s = observeWarm(s, null, 100);
		expect(s.history).toEqual([]);
		s = observeWarm(s, 1000, null);
		expect(s.history).toEqual([]);
		expect(s.lastWarmAt).toBe(1000);
	});
});

describe("secondsUntilNext (authoritative, from nextWarmAt)", () => {
	it("is null when nothing is scheduled (nextWarmAt null)", () => {
		expect(secondsUntilNext(null, 5000)).toBeNull();
	});

	it("counts down to nextWarmAt, floored at 0", () => {
		expect(secondsUntilNext(10_000, 10_000)).toBe(0);
		expect(secondsUntilNext(250_000, 10_000)).toBe(240);
		expect(secondsUntilNext(70_000, 10_000)).toBe(60);
		expect(secondsUntilNext(5_000, 999_999)).toBe(0); // already past
	});
});
