import { act, fireEvent, render, screen, within } from "@testing-library/react";
import Checkbox, { CheckboxGroup } from "./Checkbox";

/**
 * The drawn box, which is the label's one child that does not hold the hidden
 * input. StyleX compiles to class names and jsdom loads no stylesheet, so the
 * class list is the only place the composed ring is observable.
 */
function indicator(input: HTMLElement): HTMLElement {
	const label = input.closest("label") as HTMLElement;
	const spans = [...label.querySelectorAll("span")];
	const box = spans.find((span) => !span.contains(input));
	if (box === undefined) throw new Error("the checkbox drew no indicator");
	return box;
}

test("a standalone checkbox reports its state and reads its label as its name", () => {
	const onChange = vi.fn();
	render(
		<Checkbox isSelected onChange={onChange}>
			Safe search
		</Checkbox>,
	);

	const box = screen.getByRole("checkbox", { name: "Safe search" }) as HTMLInputElement;
	expect(box.checked).toBe(true);

	fireEvent.click(box);
	// The caller owns the state, so the box reports the value it would move to
	// rather than moving there itself.
	expect(onChange).toHaveBeenCalledWith(false);
});

test("a disabled checkbox keeps its state on screen but takes no input", () => {
	render(
		<Checkbox isSelected isDisabled onChange={vi.fn()}>
			Safe search
		</Checkbox>,
	);

	const box = screen.getByRole("checkbox", { name: "Safe search" }) as HTMLInputElement;
	expect(box.checked).toBe(true);
	// The disabled input is the whole guard: a browser fires no click on one, and
	// it is out of the tab order. Clicking it here would prove nothing either way,
	// because `fireEvent` dispatches straight at the node and skips that check.
	expect(box.disabled).toBe(true);
});

test("keyboard focus composes the ring onto the drawn box", () => {
	render(
		<Checkbox isSelected={false} onChange={vi.fn()}>
			Ads
		</Checkbox>,
	);

	const input = screen.getByRole("checkbox", { name: "Ads" });
	const box = indicator(input);
	const idle = box.className.split(" ");
	expect(input.closest("label")?.getAttribute("data-focus-visible")).toBeNull();

	// React Aria only calls focus visible after the modality is keyboard, which
	// is why a bare focus() is not enough to raise the ring.
	act(() => {
		fireEvent.keyDown(document.body, { key: "Tab" });
		input.focus();
	});

	expect(input.closest("label")?.getAttribute("data-focus-visible")).toBe("true");
	const ringed = box.className.split(" ");
	// The box gained classes it did not have: the ring is composed onto it, not
	// merely reported by React Aria on the root.
	expect(ringed.length).toBeGreaterThan(idle.length);
	expect(idle.every((name) => ringed.includes(name))).toBe(true);
});

test("inside a group the group owns the selection, and the group carries the name", () => {
	const onChange = vi.fn();
	render(
		<CheckboxGroup aria-label="Assigned sources" value={["1"]} onChange={onChange}>
			<Checkbox value="1">Ads</Checkbox>
			<Checkbox value="2">Trackers</Checkbox>
		</CheckboxGroup>,
	);

	const group = screen.getByRole("group", { name: "Assigned sources" });
	expect((within(group).getByRole("checkbox", { name: "Ads" }) as HTMLInputElement).checked).toBe(true);
	expect((within(group).getByRole("checkbox", { name: "Trackers" }) as HTMLInputElement).checked).toBe(false);

	// The group reports the whole set, not the box that moved.
	fireEvent.click(within(group).getByRole("checkbox", { name: "Trackers" }));
	expect(onChange).toHaveBeenCalledWith(["1", "2"]);
});
