import { describe, expect, it } from "vitest";
import type { ConversationState } from "./pure.js";
import {
	buildConversationSpec,
	buildDefaultSpec,
	computeCachePct,
	computeExpectedCacheRate,
	isTokenCurrent,
	MIN_INTERVAL_MS,
	msToSeconds,
	parseIntervalPayload,
	parseSettings,
	secondsToMs,
	serializeSettings,
	shouldWarm,
} from "./pure.js";

describe("computeCachePct", () => {
	it("cacheRead/input rounded and clamped to 0..100", () => {
		expect(computeCachePct(1000, 800)).toBe(80);
		expect(computeCachePct(1000, 1200)).toBe(100);
		expect(computeCachePct(1000, -100)).toBe(0);
		expect(computeCachePct(1000, 0)).toBe(0);
		expect(computeCachePct(1000, 333)).toBe(33);
	});

	it("zero input tokens → 0", () => {
		expect(computeCachePct(0, 500)).toBe(0);
		expect(computeCachePct(-1, 500)).toBe(0);
	});
});

describe("computeExpectedCacheRate", () => {
	it("cacheRead/(cacheRead+cacheWrite) rounded", () => {
		expect(computeExpectedCacheRate(800, 200)).toBe(80);
		expect(computeExpectedCacheRate(500, 500)).toBe(50);
		expect(computeExpectedCacheRate(1000, 0)).toBe(100);
		expect(computeExpectedCacheRate(0, 1000)).toBe(0);
		expect(computeExpectedCacheRate(333, 667)).toBe(33);
	});

	it("0 when cacheRead+cacheWrite is 0", () => {
		expect(computeExpectedCacheRate(0, 0)).toBe(0);
	});
});

describe("shouldWarm", () => {
	it("returns true when enabled, idle, and token matches", () => {
		const state: ConversationState = {
			enabled: true,
			intervalMs: 240_000,
			active: false,
			lastPct: null,
			lastExpectedPct: null,
			lastWarmAt: null,
			nextWarmAt: null,
			token: 5,
		};
		expect(shouldWarm(state, 5)).toBe(true);
	});

	it("returns false when disabled", () => {
		const state: ConversationState = {
			enabled: false,
			intervalMs: 240_000,
			active: false,
			lastPct: null,
			lastExpectedPct: null,
			lastWarmAt: null,
			nextWarmAt: null,
			token: 5,
		};
		expect(shouldWarm(state, 5)).toBe(false);
	});

	it("returns false when active", () => {
		const state: ConversationState = {
			enabled: true,
			intervalMs: 240_000,
			active: true,
			lastPct: null,
			lastExpectedPct: null,
			lastWarmAt: null,
			nextWarmAt: null,
			token: 5,
		};
		expect(shouldWarm(state, 5)).toBe(false);
	});

	it("returns false when token is superseded", () => {
		const state: ConversationState = {
			enabled: true,
			intervalMs: 240_000,
			active: false,
			lastPct: null,
			lastExpectedPct: null,
			lastWarmAt: null,
			nextWarmAt: null,
			token: 5,
		};
		expect(shouldWarm(state, 6)).toBe(false);
	});
});

describe("isTokenCurrent", () => {
	it("returns true when tokens match", () => {
		expect(isTokenCurrent(5, 5)).toBe(true);
	});

	it("returns false when tokens differ", () => {
		expect(isTokenCurrent(5, 6)).toBe(false);
	});
});

describe("parseSettings/serializeSettings round-trip", () => {
	it("round-trips enabled + intervalMs", () => {
		const original = { enabled: false, intervalMs: 120_000 };
		const serialized = serializeSettings(original);
		const parsed = parseSettings(serialized);
		expect(parsed).toEqual(original);
	});

	it("returns defaults for null input (warming OFF by default — CR-4a)", () => {
		const parsed = parseSettings(null);
		expect(parsed).toEqual({ enabled: false, intervalMs: 240_000 });
	});

	it("returns defaults for malformed JSON (warming OFF by default — CR-4a)", () => {
		const parsed = parseSettings("not-json{{{");
		expect(parsed).toEqual({ enabled: false, intervalMs: 240_000 });
	});

	it("clamps non-positive interval to MIN_INTERVAL_MS", () => {
		const parsed = parseSettings(JSON.stringify({ enabled: true, intervalMs: -500 }));
		expect(parsed.intervalMs).toBe(1000);
	});

	it("uses default for NaN interval", () => {
		const parsed = parseSettings(JSON.stringify({ enabled: true, intervalMs: Number.NaN }));
		expect(parsed.intervalMs).toBe(240_000);
	});
});

describe("msToSeconds", () => {
	it("converts ms to seconds, rounded", () => {
		expect(msToSeconds(240_000)).toBe(240);
		expect(msToSeconds(1500)).toBe(2);
		expect(msToSeconds(1000)).toBe(1);
		expect(msToSeconds(0)).toBe(0);
	});
});

describe("secondsToMs", () => {
	it("converts seconds to ms, floors at MIN_INTERVAL_MS", () => {
		expect(secondsToMs(240)).toBe(240_000);
		expect(secondsToMs(1)).toBe(1000);
		expect(secondsToMs(0.5)).toBe(MIN_INTERVAL_MS);
	});

	it("returns null for NaN / non-positive", () => {
		expect(secondsToMs(Number.NaN)).toBeNull();
		expect(secondsToMs(0)).toBeNull();
		expect(secondsToMs(-5)).toBeNull();
		expect(secondsToMs(Number.POSITIVE_INFINITY)).toBeNull();
	});
});

describe("parseIntervalPayload", () => {
	it("accepts a bare positive number", () => {
		expect(parseIntervalPayload(30)).toBe(30);
		expect(parseIntervalPayload(1)).toBe(1);
	});

	it("accepts { value: number }", () => {
		expect(parseIntervalPayload({ value: 30 })).toBe(30);
		expect(parseIntervalPayload({ value: 1 })).toBe(1);
	});

	it("returns null for NaN / non-positive / wrong shape", () => {
		expect(parseIntervalPayload(Number.NaN)).toBeNull();
		expect(parseIntervalPayload(0)).toBeNull();
		expect(parseIntervalPayload(-5)).toBeNull();
		expect(parseIntervalPayload("30")).toBeNull();
		expect(parseIntervalPayload({ value: "30" })).toBeNull();
		expect(parseIntervalPayload({})).toBeNull();
		expect(parseIntervalPayload(null)).toBeNull();
		expect(parseIntervalPayload(undefined)).toBeNull();
	});
});

describe("buildConversationSpec", () => {
	it("builds a per-conversation spec with toggle + number(interval) + last-% + retention + timer fields", () => {
		const spec = buildConversationSpec(true, 240_000, 80, 95, 1000, 500);
		expect(spec.id).toBe("cache-warming");
		expect(spec.region).toBe("side");
		expect(spec.title).toBe("Cache Warming");
		expect(spec.fields).toHaveLength(5);

		const toggle = spec.fields[0];
		expect(toggle).toEqual({
			kind: "toggle",
			label: "Enabled",
			value: true,
			action: { actionId: "cache-warming/toggle" },
		});

		const number = spec.fields[1];
		expect(number).toEqual({
			kind: "number",
			label: "Refresh Interval",
			value: 240,
			min: 1,
			step: 1,
			unit: "s",
			action: { actionId: "cache-warming/set-interval" },
		});

		const stat = spec.fields[2];
		expect(stat).toEqual({
			kind: "stat",
			label: "Last Cache %",
			value: "80%",
		});

		const retention = spec.fields[3];
		expect(retention).toEqual({
			kind: "stat",
			label: "Cache retention",
			value: "95%",
		});

		const timer = spec.fields[4];
		expect(timer).toEqual({
			kind: "custom",
			rendererId: "cache-warming-timer",
			payload: { nextWarmAt: 1000, lastWarmAt: 500 },
		});
	});

	it("shows — when lastPct and lastExpectedPct are null", () => {
		const spec = buildConversationSpec(true, 240_000, null, null, null, null);
		const stat = spec.fields[2];
		expect(stat).toEqual({
			kind: "stat",
			label: "Last Cache %",
			value: "—",
		});
		const retention = spec.fields[3];
		expect(retention).toEqual({
			kind: "stat",
			label: "Cache retention",
			value: "—",
		});
		const timer = spec.fields[4];
		expect(timer).toEqual({
			kind: "custom",
			rendererId: "cache-warming-timer",
			payload: { nextWarmAt: null, lastWarmAt: null },
		});
	});

	it("reflects disabled state", () => {
		const spec = buildConversationSpec(false, 120_000, 50, 75, null, null);
		const toggle = spec.fields[0];
		expect(toggle).toEqual({
			kind: "toggle",
			label: "Enabled",
			value: false,
			action: { actionId: "cache-warming/toggle" },
		});
		const number = spec.fields[1];
		expect(number).toEqual({
			kind: "number",
			label: "Refresh Interval",
			value: 120,
			min: 1,
			step: 1,
			unit: "s",
			action: { actionId: "cache-warming/set-interval" },
		});
	});
});

describe("buildDefaultSpec", () => {
	it("returns a default spec with no conversationId", () => {
		const spec = buildDefaultSpec();
		expect(spec.id).toBe("cache-warming");
		expect(spec.region).toBe("side");
		expect(spec.title).toBe("Cache Warming");
		expect(spec.fields).toHaveLength(1);
		expect(spec.fields[0]).toEqual({
			kind: "stat",
			label: "Status",
			value: "No conversation focused",
		});
	});
});
