import type { SurfaceField, SurfaceSpec } from "@dispatch/ui-contract";
import { describe, expect, it } from "vitest";
import { buildInvoke, groupRenderFields, planSurface } from "./plan";
import type { FieldView } from "./types";

const makeSpec = (...fields: SurfaceField[]): SurfaceSpec => ({
	id: "test-surface",
	region: "test",
	title: "Test Surface",
	fields,
});

describe("planSurface", () => {
	it("maps a toggle field to a ToggleFieldView", () => {
		const plan = planSurface(
			makeSpec({ kind: "toggle", label: "Dark mode", value: true, action: { actionId: "dm" } }),
		);
		expect(plan.fields).toEqual([
			{ kind: "toggle", label: "Dark mode", value: true, action: { actionId: "dm" } },
		]);
	});

	it("maps a progress field to a ProgressFieldView", () => {
		const plan = planSurface(makeSpec({ kind: "progress", label: "Loading", value: 0.42 }));
		expect(plan.fields).toEqual([{ kind: "progress", label: "Loading", value: 0.42 }]);
	});

	it("maps a selector field to a SelectorFieldView", () => {
		const plan = planSurface(
			makeSpec({
				kind: "selector",
				label: "Model",
				value: "gpt-4",
				options: [
					{ value: "gpt-4", label: "GPT-4" },
					{ value: "gpt-3.5", label: "GPT-3.5" },
				],
				action: { actionId: "set-model" },
			}),
		);
		expect(plan.fields).toEqual([
			{
				kind: "selector",
				label: "Model",
				value: "gpt-4",
				options: [
					{ value: "gpt-4", label: "GPT-4" },
					{ value: "gpt-3.5", label: "GPT-3.5" },
				],
				action: { actionId: "set-model" },
			},
		]);
	});

	it("maps a stat field to a StatFieldView", () => {
		const plan = planSurface(makeSpec({ kind: "stat", label: "Tokens", value: "1,234" }));
		expect(plan.fields).toEqual([{ kind: "stat", label: "Tokens", value: "1,234" }]);
	});

	it("maps a number field to a NumberFieldView, carrying optional hints", () => {
		const plan = planSurface(
			makeSpec({
				kind: "number",
				label: "Interval",
				value: 240,
				min: 1,
				step: 1,
				unit: "s",
				action: { actionId: "cache-warming/set-interval" },
			}),
		);
		expect(plan.fields).toEqual([
			{
				kind: "number",
				label: "Interval",
				value: 240,
				min: 1,
				step: 1,
				unit: "s",
				action: { actionId: "cache-warming/set-interval" },
			},
		]);
	});

	it("omits absent number hints (no max key when undefined)", () => {
		const plan = planSurface(
			makeSpec({
				kind: "number",
				label: "Interval",
				value: 240,
				min: 1,
				action: { actionId: "set" },
			}),
		);
		const field = plan.fields[0];
		expect(field).not.toHaveProperty("max");
		expect(field).not.toHaveProperty("step");
		expect(field).not.toHaveProperty("unit");
	});

	it("maps a button field to a ButtonFieldView", () => {
		const plan = planSurface(
			makeSpec({ kind: "button", label: "Retry", action: { actionId: "retry" } }),
		);
		expect(plan.fields).toEqual([
			{ kind: "button", label: "Retry", action: { actionId: "retry" } },
		]);
	});

	it("preserves field order", () => {
		const plan = planSurface(
			makeSpec(
				{ kind: "stat", label: "A", value: "1" },
				{ kind: "toggle", label: "B", value: false, action: { actionId: "b" } },
				{ kind: "progress", label: "C", value: 0.5 },
				{ kind: "button", label: "D", action: { actionId: "d" } },
			),
		);
		expect(plan.fields.map((f) => ("label" in f ? f.label : null))).toEqual(["A", "B", "C", "D"]);
	});

	it("drops unknown field kinds gracefully", () => {
		const plan = planSurface(
			makeSpec({ kind: "stat", label: "Known", value: "ok" }, {
				kind: "future-kind" as "stat",
				label: "Unknown",
				value: "?",
			} as SurfaceField),
		);
		expect(plan.fields).toHaveLength(1);
		const first = plan.fields[0];
		expect(first && "label" in first ? first.label : null).toBe("Known");
	});

	it("carries custom fields through verbatim, preserving order", () => {
		const plan = planSurface(
			makeSpec(
				{ kind: "stat", label: "Before", value: "1" },
				{ kind: "custom", rendererId: "chart", payload: { data: [1, 2, 3] } },
				{ kind: "stat", label: "After", value: "2" },
			),
		);
		expect(plan.fields).toHaveLength(3);
		expect(plan.fields[1]).toEqual({
			kind: "custom",
			rendererId: "chart",
			payload: { data: [1, 2, 3] },
		});
	});

	it("returns empty fields for an empty spec", () => {
		const plan = planSurface(makeSpec());
		expect(plan.fields).toEqual([]);
	});

	it("keeps every custom field (render-time decides whether to show each)", () => {
		const plan = planSurface(
			makeSpec(
				{ kind: "custom", rendererId: "x", payload: null },
				{ kind: "custom", rendererId: "y", payload: 42 },
			),
		);
		expect(plan.fields.map((f) => f.kind)).toEqual(["custom", "custom"]);
	});
});

describe("groupRenderFields", () => {
	const stat = (label: string, value: string): FieldView => ({ kind: "stat", label, value });
	const toggle = (label: string): FieldView => ({
		kind: "toggle",
		label,
		value: false,
		action: { actionId: label },
	});

	it("coalesces consecutive stats into a single stats group", () => {
		const groups = groupRenderFields([stat("a", "1"), stat("b", "2"), stat("c", "3")]);
		expect(groups).toHaveLength(1);
		expect(groups[0]).toEqual({
			type: "stats",
			stats: [
				{ kind: "stat", label: "a", value: "1" },
				{ kind: "stat", label: "b", value: "2" },
				{ kind: "stat", label: "c", value: "3" },
			],
		});
	});

	it("keeps non-stat fields as standalone groups and preserves order", () => {
		const groups = groupRenderFields([stat("a", "1"), toggle("t"), stat("b", "2")]);
		expect(groups.map((g) => g.type)).toEqual(["stats", "field", "stats"]);
		const first = groups[0];
		const last = groups[2];
		if (first?.type !== "stats" || last?.type !== "stats") throw new Error("bad grouping");
		expect(first.stats.map((s) => s.label)).toEqual(["a"]);
		expect(last.stats.map((s) => s.label)).toEqual(["b"]);
	});

	it("starts a new stats run after an interrupting field", () => {
		const groups = groupRenderFields([stat("a", "1"), stat("b", "2"), toggle("t"), stat("c", "3")]);
		expect(groups.map((g) => g.type)).toEqual(["stats", "field", "stats"]);
	});

	it("returns no groups for an empty field list", () => {
		expect(groupRenderFields([])).toEqual([]);
	});
});

describe("buildInvoke", () => {
	it("builds an invoke message for a toggle field", () => {
		const field = { kind: "toggle" as const, label: "T", value: false, action: { actionId: "t" } };
		const msg = buildInvoke("s1", field, true);
		expect(msg).toEqual({ type: "invoke", surfaceId: "s1", actionId: "t", payload: true });
	});

	it("builds an invoke message for a selector field", () => {
		const field = {
			kind: "selector" as const,
			label: "S",
			value: "a",
			options: [],
			action: { actionId: "sel" },
		};
		const msg = buildInvoke("s1", field, "b");
		expect(msg).toEqual({ type: "invoke", surfaceId: "s1", actionId: "sel", payload: "b" });
	});

	it("builds an invoke message without payload for a button field", () => {
		const field = { kind: "button" as const, label: "B", action: { actionId: "btn" } };
		const msg = buildInvoke("s1", field);
		expect(msg).toEqual({ type: "invoke", surfaceId: "s1", actionId: "btn" });
	});

	it("omits payload key when value is undefined", () => {
		const field = { kind: "button" as const, label: "B", action: { actionId: "btn" } };
		const msg = buildInvoke("s1", field, undefined);
		expect(msg).not.toHaveProperty("payload");
	});

	it("uses the field's actionId, not a surface-level id", () => {
		const field = {
			kind: "toggle" as const,
			label: "X",
			value: true,
			action: { actionId: "custom-action-123" },
		};
		const msg = buildInvoke("surf", field, false);
		expect(msg.actionId).toBe("custom-action-123");
	});
});
