/**
 * Frontend seam test: drive a real React component against a fixture derived
 * from the actual RPC response (the canonical `CONTRACT_FIXTURES` shared with
 * the Rust side via the snapshot file).
 *
 * The first test renders `SettingsPage` against the well-formed fixture and
 * asserts the form populates with values from the RPC response — proving the
 * backend ↔ frontend wire shape lines up end-to-end without hand-rolled
 * fixtures.
 *
 * The second test feeds a *malformed* RPC response (a frame missing the
 * required envelope `ok` field) and asserts the `rpc.ts` client surfaces a
 * visible error in the rendered UI instead of leaving the page empty.
 */
import { afterEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { SettingsPage } from "./SettingsPage";
import { CONTRACT_FIXTURES } from "../api/rpcContract";
import snapshot from "../api/rpcContract.snapshot.json";

afterEach(() => {
	vi.restoreAllMocks();
});

interface MockSocket {
	url: string;
	onopen: ((ev: Event) => void) | null;
	onmessage: ((ev: { data: string }) => void) | null;
	onerror: ((ev: Event) => void) | null;
	onclose: ((ev: CloseEvent) => void) | null;
	readyState: number;
	send(data: string): void;
	close(): void;
}

/**
 * Install a `WebSocket` shim that hands each registered method a single
 * canned frame.  Callers register either a normal RPC result or a
 * deliberately malformed frame body (returned verbatim — i.e. the body
 * literally has no `ok` field, simulating a server bug).
 */
function installSeamWs(replies: {
	[method: string]: { kind: "ok"; result: unknown } | { kind: "raw"; body: object };
}) {
	const instances: MockSocket[] = [];
	class SeamWs implements MockSocket {
		static readonly CONNECTING = 0;
		static readonly OPEN = 1;
		static readonly CLOSING = 2;
		static readonly CLOSED = 3;
		url: string;
		onopen: ((ev: Event) => void) | null = null;
		onmessage: ((ev: { data: string }) => void) | null = null;
		onerror: ((ev: Event) => void) | null = null;
		onclose: ((ev: CloseEvent) => void) | null = null;
		readyState = 0;
		constructor(url: string) {
			this.url = url;
			instances.push(this);
			queueMicrotask(() => {
				this.readyState = 1;
				this.onopen?.(new Event("open"));
			});
		}
		send(data: string) {
			let frame: {
				correlation_id?: string;
				method?: string;
			};
			try {
				frame = JSON.parse(data);
			} catch {
				return;
			}
			const { correlation_id, method } = frame;
			if (!correlation_id || !method) return;
			queueMicrotask(() => {
				const reply = replies[method];
				if (!reply) {
					this.onmessage?.({
						data: JSON.stringify({
							kind: "rpc_response",
							version: 1,
							correlation_id,
							ok: false,
							error: `no fixture for ${method}`,
							code: "NOT_FOUND",
						}),
					});
					return;
				}
				if (reply.kind === "ok") {
					this.onmessage?.({
						data: JSON.stringify({
							kind: "rpc_response",
							version: 1,
							correlation_id,
							ok: true,
							result: reply.result,
						}),
					});
					return;
				}
				// raw: deliberately malformed envelope (no `ok` field)
				this.onmessage?.({
					data: JSON.stringify({
						kind: "rpc_response",
						version: 1,
						correlation_id,
						...reply.body,
					}),
				});
			});
		}
		close() {
			this.readyState = 3;
		}
	}
	vi.stubGlobal("WebSocket", SeamWs);
	return instances;
}

describe("SettingsPage seam test", () => {
	it("renders ProjectSettings from the typed RPC contract fixture", async () => {
		// Sanity: the in-source fixture mirrors the on-disk snapshot file. If
		// this trips, the contract has drifted from the Rust side.
		expect(CONTRACT_FIXTURES["settings.put_project"].result).toEqual(
			snapshot["settings.put_project"].result,
		);

		const fixture = CONTRACT_FIXTURES["settings.put_project"].result;
		installSeamWs({
			"settings.get_project": { kind: "ok", result: fixture },
		});

		const onBack = vi.fn();
		render(<SettingsPage onBack={onBack} />);

		await waitFor(() => {
			expect(screen.getByDisplayValue(String(fixture.max_retries))).toBeInTheDocument();
		});

		// Field driven directly by the RPC payload populates the form.
		expect(
			screen.getByDisplayValue(String(fixture.watcher_sweep_interval_secs)),
		).toBeInTheDocument();
		expect(
			screen.getByDisplayValue(String(fixture.watcher_done_retention_secs)),
		).toBeInTheDocument();
	});

	it("shows a visible error when the RPC response is malformed", async () => {
		// `body` lacks the envelope `ok` field. The fixed `rpc.ts` client
		// should reject loudly with a `MALFORMED` error instead of letting
		// the page render empty.
		installSeamWs({
			"settings.get_project": {
				kind: "raw",
				body: { result: { not_actually_settings: true } },
			},
		});

		const onBack = vi.fn();
		render(<SettingsPage onBack={onBack} />);

		await waitFor(() => {
			expect(screen.getByText(/Malformed RPC response/i)).toBeInTheDocument();
		});

		// And critically — no empty form is rendered.
		expect(screen.queryByText(/default qa/i)).not.toBeInTheDocument();
	});

	it("user can edit and the new value flows through settings.put_project RPC", async () => {
		const fixture = CONTRACT_FIXTURES["settings.put_project"].result;
		const updated = { ...fixture, max_retries: 9 };
		installSeamWs({
			"settings.get_project": { kind: "ok", result: fixture },
			"settings.put_project": { kind: "ok", result: updated },
		});

		const onBack = vi.fn();
		render(<SettingsPage onBack={onBack} />);

		const maxRetriesInput = (await screen.findByDisplayValue(
			String(fixture.max_retries),
		)) as HTMLInputElement;

		fireEvent.change(maxRetriesInput, { target: { value: "9" } });
		fireEvent.click(screen.getByRole("button", { name: /save/i }));

		await waitFor(() => {
			expect(screen.getByDisplayValue("9")).toBeInTheDocument();
		});
	});
});
