import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Settings } from "../Settings";
import type { MonitoringMachine } from "../../types";

const saveMachineMutate = vi.fn().mockResolvedValue({});
const deleteMachineMutate = vi.fn();
const testSSHMutate = vi.fn().mockResolvedValue({
	message: "SSH auth succeeded",
	known_hosts_updated: true,
});

let machines: MonitoringMachine[] = [];

vi.mock("../../hooks/useSettings", () => ({
	useMonitoringSettings: () => ({ data: machines }),
	useSSHKeys: () => ({ data: [] }),
	useSaveMonitoringMachine: () => ({
		mutateAsync: saveMachineMutate,
		isPending: false,
	}),
	useDeleteMonitoringMachine: () => ({ mutate: deleteMachineMutate }),
	useTestMonitoringMachineSSH: () => ({
		mutateAsync: testSSHMutate,
		isPending: false,
	}),
	useResetLocalDatabase: () => ({}),
	useSaveSSHKey: () => ({ mutateAsync: vi.fn() }),
	useGenerateSSHKey: () => ({ mutateAsync: vi.fn(), isPending: false }),
	useDeleteSSHKey: () => ({ mutate: vi.fn() }),
}));

function localMachine(
	overrides: Partial<MonitoringMachine> = {},
): MonitoringMachine {
	return {
		id: "m1",
		name: "This machine",
		mode: "local",
		enabled: true,
		services: ["monitoring", "files", "jellyfin"],
		host: "",
		port: 22,
		username: "",
		key_directory: "",
		key_name: "",
		ssh_key_id: "",
		ssh_private_key_set: false,
		ssh_private_key_passphrase_set: false,
		password_set: false,
		notes: "Primary node",
		...overrides,
	} as MonitoringMachine;
}

beforeEach(() => {
	saveMachineMutate.mockClear();
	deleteMachineMutate.mockClear();
	testSSHMutate.mockClear();
	machines = [];
});

describe("Settings", () => {
	it("renders the machine list from the mocked store", () => {
		machines = [localMachine()];
		render(<Settings />);
		// The rail row caption (mode · enabled) is unique to the selection rail.
		expect(screen.getByText("local · Enabled")).toBeInTheDocument();
	});

	it("saves a machine via the editor dialog (controlled useState parity)", async () => {
		machines = [localMachine()];
		render(<Settings />);

		// The detail-pane "Edit" has visible text "Edit"; the rail hover edit
		// affordance is icon-only (aria-label "Edit") — disambiguate by text.
		const detailEdit = screen
			.getAllByRole("button", { name: "Edit" })
			.find((button) => button.textContent === "Edit") as HTMLButtonElement;
		await userEvent.click(detailEdit);

		expect(screen.getByText("Edit machine")).toBeInTheDocument();

		// Rename through the labeled field, then save.
		const nameInput = screen.getByLabelText("Name");
		await userEvent.clear(nameInput);
		await userEvent.type(nameInput, "Worker node");

		await userEvent.click(screen.getByRole("button", { name: "Save machine" }));

		expect(saveMachineMutate).toHaveBeenCalledTimes(1);
		const saved = saveMachineMutate.mock.calls[0][0];
		expect(saved.name).toBe("Worker node");
		expect(saved.mode).toBe("local");
	});

	it("deletes a machine through the confirm dialog", async () => {
		machines = [localMachine()];
		render(<Settings />);

		// Detail-pane "Delete" opens the confirm dialog.
		await userEvent.click(screen.getByRole("button", { name: "Delete" }));
		expect(screen.getByText("Delete machine?")).toBeInTheDocument();

		// Confirm (the confirm dialog's "Delete" is the last one rendered).
		const deletes = screen.getAllByRole("button", { name: "Delete" });
		await userEvent.click(deletes[deletes.length - 1]);

		expect(deleteMachineMutate).toHaveBeenCalledTimes(1);
		expect(deleteMachineMutate).toHaveBeenCalledWith("m1");
	});
});

// jsdom has no window.matchMedia; default to desktop so existing tests are
// unaffected.
function setMatchMedia(matches: boolean) {
	window.matchMedia = ((query: string) => ({
		matches: query.includes("768") ? matches : false,
		media: query,
		onchange: null,
		addEventListener: () => {},
		removeEventListener: () => {},
		addListener: () => {},
		removeListener: () => {},
		dispatchEvent: () => false,
	})) as unknown as typeof window.matchMedia;
}

describe("Settings (mobile SheetForm — slice 7)", () => {
	beforeEach(() => setMatchMedia(true));

	it("opens the machine editor in a SheetForm below md", async () => {
		machines = [localMachine()];
		render(<Settings />);

		// Open the editor via the detail-pane Edit button (visible text).
		const detailEdit = screen
			.getAllByRole("button", { name: "Edit" })
			.find((button) => button.textContent === "Edit") as HTMLButtonElement;
		await userEvent.click(detailEdit);

		// SheetForm renders a dialog; the DialogTitle shows the editor title.
		expect(screen.getByText("Edit machine")).toBeInTheDocument();
		expect(screen.getByRole("dialog")).toBeInTheDocument();
		// Desktop DialogDescription text is not rendered as a dialog description
		// on mobile (the MachineEditor has its own hint labels, which is fine).
		expect(
			screen.queryByRole("heading", { name: "Create machine" }),
		).not.toBeInTheDocument();
	});

	it("saves a machine via the SheetForm on mobile", async () => {
		machines = [localMachine()];
		render(<Settings />);

		const detailEdit = screen
			.getAllByRole("button", { name: "Edit" })
			.find((button) => button.textContent === "Edit") as HTMLButtonElement;
		await userEvent.click(detailEdit);

		const nameInput = screen.getByLabelText("Name");
		await userEvent.clear(nameInput);
		await userEvent.type(nameInput, "Renamed node");

		await userEvent.click(screen.getByRole("button", { name: "Save machine" }));

		expect(saveMachineMutate).toHaveBeenCalledTimes(1);
		const saved = saveMachineMutate.mock.calls[0][0];
		expect(saved.name).toBe("Renamed node");
		expect(saved.mode).toBe("local");
	});

	it("cancel closes the SheetForm on mobile", async () => {
		machines = [localMachine()];
		render(<Settings />);

		const detailEdit = screen
			.getAllByRole("button", { name: "Edit" })
			.find((button) => button.textContent === "Edit") as HTMLButtonElement;
		await userEvent.click(detailEdit);

		expect(screen.getByRole("dialog")).toBeInTheDocument();

		await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
		// The sheet is now closed — the dialog role should no longer be present.
		// (The page content itself is still rendered; only the sheet unmounts.)
		expect(screen.queryByText("Edit machine")).not.toBeInTheDocument();
	});

	it("prompts before discarding unsaved machine edits (R4.5)", async () => {
		machines = [localMachine()];
		render(<Settings />);

		const detailEdit = screen
			.getAllByRole("button", { name: "Edit" })
			.find((button) => button.textContent === "Edit") as HTMLButtonElement;
		await userEvent.click(detailEdit);

		// Edit the name to make the form dirty.
		const nameInput = screen.getByLabelText("Name");
		await userEvent.clear(nameInput);
		await userEvent.type(nameInput, "Dirty name");

		// Cancel should NOT immediately close — the discard confirm appears.
		await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
		expect(
			screen.getByRole("heading", { name: "Discard changes?" }),
		).toBeInTheDocument();
		// The editor is still open.
		expect(screen.getByText("Edit machine")).toBeInTheDocument();
	});
});
