// Vitest coverage for the F8-09 turn-report icon-popup section menu.
// Smokes the sticky trigger, its active-title label, the open/close
// state machine, popover items + active highlight, scroll-on-pick,
// and the Escape + outside-click dismissal paths. The
// IntersectionObserver-driven `activeSlug` is computed by the
// orchestrator (`report.svelte`) and passed in via prop; the TOC
// itself owns no observers.

import "@testing-library/jest-dom/vitest";
import { fireEvent, render } from "@testing-library/svelte";
import { beforeEach, describe, expect, test, vi } from "vitest";

import { i18n } from "../src/lib/i18n/index.svelte";
import type { TranslationKey } from "../src/lib/i18n/index.svelte";

import ReportToc, {
	type TocEntry,
} from "../src/lib/active-view/report/report-toc.svelte";

const ENTRIES: readonly TocEntry[] = [
	{ slug: "galaxy-summary", titleKey: "game.report.section.galaxy_summary.title" },
	{ slug: "votes", titleKey: "game.report.section.votes.title" },
	{ slug: "bombings", titleKey: "game.report.section.bombings.title" },
];

function stubReducedMotion(reduce: boolean): void {
	Object.defineProperty(window, "matchMedia", {
		writable: true,
		value: (query: string) => ({
			matches: reduce && query.includes("reduce"),
			media: query,
			onchange: null,
			addListener: () => {},
			removeListener: () => {},
			addEventListener: () => {},
			removeEventListener: () => {},
			dispatchEvent: () => false,
		}),
	});
}

beforeEach(() => {
	i18n.resetForTests("en");
});

describe("report TOC (icon-popup)", () => {
	test("renders the trigger with the active section title; popover closed by default", () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary" },
		});
		const trigger = ui.getByTestId("report-toc-trigger");
		expect(trigger).toHaveAttribute("aria-haspopup", "menu");
		expect(trigger).toHaveAttribute("aria-expanded", "false");
		expect(trigger).toHaveTextContent("galaxy summary");
		expect(ui.queryByTestId("report-toc-surface")).toBeNull();
	});

	test("clicking the trigger opens the popover with one menuitem per entry", async () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary" },
		});
		await fireEvent.click(ui.getByTestId("report-toc-trigger"));
		expect(ui.getByTestId("report-toc-trigger")).toHaveAttribute(
			"aria-expanded",
			"true",
		);
		const surface = ui.getByTestId("report-toc-surface");
		expect(surface).toHaveAttribute("role", "menu");
		for (const entry of ENTRIES) {
			const item = ui.getByTestId(`report-toc-item-${entry.slug}`);
			expect(item).toHaveAttribute("role", "menuitem");
		}
	});

	test("marks the active item with aria-current=location and .active", async () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "bombings" },
		});
		await fireEvent.click(ui.getByTestId("report-toc-trigger"));
		const active = ui.getByTestId("report-toc-item-bombings");
		expect(active).toHaveAttribute("aria-current", "location");
		expect(active).toHaveClass("active");

		const inactive = ui.getByTestId("report-toc-item-votes");
		expect(inactive).not.toHaveAttribute("aria-current");
		expect(inactive).not.toHaveClass("active");
	});

	test("trigger label tracks activeSlug across re-renders", async () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary" },
		});
		expect(ui.getByTestId("report-toc-trigger")).toHaveTextContent(
			"galaxy summary",
		);
		await ui.rerender({ entries: ENTRIES, activeSlug: "votes" });
		expect(ui.getByTestId("report-toc-trigger")).toHaveTextContent("votes");
	});

	test("clicking a menuitem scrolls the target into view and closes the popover", async () => {
		// jsdom does not implement `scrollIntoView`; stub it on the
		// section the spec is about to pick. `matchMedia` is forced to
		// `reduce` so the assertion stays stable.
		const scrollSpy = vi.fn();
		const target = document.createElement("section");
		target.id = "report-bombings";
		target.scrollIntoView = scrollSpy;
		document.body.appendChild(target);
		stubReducedMotion(true);

		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary" },
		});
		await fireEvent.click(ui.getByTestId("report-toc-trigger"));
		await fireEvent.click(ui.getByTestId("report-toc-item-bombings"));
		// The component defers `scrollIntoView` one frame so the
		// surface unmount + focus restoration run first. Flush the
		// frame before asserting.
		await new Promise<void>((resolve) =>
			requestAnimationFrame(() => resolve()),
		);

		expect(scrollSpy).toHaveBeenCalledWith({
			behavior: "auto",
			block: "start",
		});
		expect(ui.queryByTestId("report-toc-surface")).toBeNull();
		expect(ui.getByTestId("report-toc-trigger")).toHaveAttribute(
			"aria-expanded",
			"false",
		);
		target.remove();
	});

	test("Escape closes the popover", async () => {
		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary" },
		});
		await fireEvent.click(ui.getByTestId("report-toc-trigger"));
		expect(ui.getByTestId("report-toc-surface")).toBeInTheDocument();
		await fireEvent.keyDown(document, { key: "Escape" });
		expect(ui.queryByTestId("report-toc-surface")).toBeNull();
	});

	test("outside click closes the popover", async () => {
		const outside = document.createElement("button");
		document.body.appendChild(outside);

		const ui = render(ReportToc, {
			props: { entries: ENTRIES, activeSlug: "galaxy-summary" },
		});
		await fireEvent.click(ui.getByTestId("report-toc-trigger"));
		expect(ui.getByTestId("report-toc-surface")).toBeInTheDocument();
		await fireEvent.click(outside);
		expect(ui.queryByTestId("report-toc-surface")).toBeNull();

		outside.remove();
	});

	// Soft type-level check: a stray `as unknown as ...` cast on
	// `TocEntry` would silently widen the prop shape — assert the
	// runtime values look as expected.
	test("TocEntry exposes a slug and a TranslationKey", () => {
		const slug: string = ENTRIES[0]!.slug;
		const key: TranslationKey = ENTRIES[0]!.titleKey;
		expect(typeof slug).toBe("string");
		expect(typeof key).toBe("string");
	});
});
